Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
8ff86b1e29 | |||
e1b8760ee3 | |||
bd0d890b2c | |||
2a2118d769 | |||
9839b64650 | |||
2bf55e12f9 | |||
2249fa9232 | |||
f673a65304 | |||
0163459ad6 | |||
b21123cc9d | |||
7800d125b2 | |||
89ea648f18 | |||
ab7ac3c2d0 | |||
b16319d962 | |||
f8012d5dfb | |||
45a2015597 |
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,3 +1,20 @@
|
|||||||
|
### **1.0.1** (2021-05-01)
|
||||||
|
|
||||||
|
- Fix: LoginOtp (and not otc)
|
||||||
|
|
||||||
|
# **1.0.0** (2021-05-01)
|
||||||
|
|
||||||
|
- #4: Guide for implementing a missing page
|
||||||
|
- Support OTP #4
|
||||||
|
|
||||||
|
### **0.4.4** (2021-04-29)
|
||||||
|
|
||||||
|
- Fix previous release
|
||||||
|
|
||||||
|
### **0.4.3** (2021-04-29)
|
||||||
|
|
||||||
|
- Add infos about the plugin that defines authorizedMailDomains
|
||||||
|
|
||||||
### **0.4.2** (2021-04-29)
|
### **0.4.2** (2021-04-29)
|
||||||
|
|
||||||
- Client side validation of allowed email domains
|
- Client side validation of allowed email domains
|
||||||
|
20
README.md
20
README.md
@ -56,6 +56,7 @@ If you already have a Keycloak custom theme, it can be easily ported to Keycloak
|
|||||||
- [Hot reload](#hot-reload)
|
- [Hot reload](#hot-reload)
|
||||||
- [Enable loading in a blink of an eye of login pages ⚡ (--external-assets)](#enable-loading-in-a-blink-of-an-eye-of-login-pages----external-assets)
|
- [Enable loading in a blink of an eye of login pages ⚡ (--external-assets)](#enable-loading-in-a-blink-of-an-eye-of-login-pages----external-assets)
|
||||||
- [Support for Terms and conditions](#support-for-terms-and-conditions)
|
- [Support for Terms and conditions](#support-for-terms-and-conditions)
|
||||||
|
- [Some pages still have the default theme. Why?](#some-pages-still-have-the-default-theme-why)
|
||||||
- [GitHub Actions](#github-actions)
|
- [GitHub Actions](#github-actions)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
- [Limitations](#limitations)
|
- [Limitations](#limitations)
|
||||||
@ -178,6 +179,14 @@ You can find an example of such customization [here](https://github.com/InseeFrL
|
|||||||
|
|
||||||
And you can test the result in production by trying the login register page of [Onyxia](https://datalab.sspcloud.fr)
|
And you can test the result in production by trying the login register page of [Onyxia](https://datalab.sspcloud.fr)
|
||||||
|
|
||||||
|
WARNING: If you chose to go this way use:
|
||||||
|
```json
|
||||||
|
"dependencies": {
|
||||||
|
"keycloakify": "~X.Y.Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
in your `package.json` instead of `^X.Y.Z`. A minor release might break your app.
|
||||||
|
|
||||||
### Hot reload
|
### Hot reload
|
||||||
|
|
||||||
Rebuild the theme each time you make a change to see the result is not practical.
|
Rebuild the theme each time you make a change to see the result is not practical.
|
||||||
@ -230,6 +239,15 @@ First you need to enable the required action on the Keycloak server admin consol
|
|||||||
|
|
||||||
Then to load your own therms of services using [like this](https://github.com/garronej/keycloakify-demo-app/blob/8168c928a66605f2464f9bd28a4dc85fb0a231f9/src/index.tsx#L42-L66).
|
Then to load your own therms of services using [like this](https://github.com/garronej/keycloakify-demo-app/blob/8168c928a66605f2464f9bd28a4dc85fb0a231f9/src/index.tsx#L42-L66).
|
||||||
|
|
||||||
|
# Some pages still have the default theme. Why?
|
||||||
|
|
||||||
|
This project only support the most common user facing pages of Keycloak login.
|
||||||
|
[Here is](https://user-images.githubusercontent.com/6702424/116784128-d4f97f00-aa92-11eb-92c9-b024c2521aa3.png) the complete list of pages.
|
||||||
|
And [here](https://github.com/InseeFrLab/keycloakify/tree/main/src/lib/components) are the pages currently implemented by this module.
|
||||||
|
If you need to customize pages that are not supported yet you can submit an issue about it and wait for me get it implemented.
|
||||||
|
If you can't wait, PR are welcome! [Here](https://github.com/InseeFrLab/keycloakify/commit/0163459ad6b1ad0afcc34fae5f3cc28dbcf8b4a7) is the commit that adds support
|
||||||
|
for the `login-otp.ftl` page. You can use it as a model for implementing other pages.
|
||||||
|
|
||||||
# GitHub Actions
|
# GitHub Actions
|
||||||
|
|
||||||

|

|
||||||
@ -261,6 +279,8 @@ and a `build/static/` directory generated by webpack.
|
|||||||
You won't be able to [import things from your public directory **in your JavaScript code**](https://create-react-app.dev/docs/using-the-public-folder/#adding-assets-outside-of-the-module-system).
|
You won't be able to [import things from your public directory **in your JavaScript code**](https://create-react-app.dev/docs/using-the-public-folder/#adding-assets-outside-of-the-module-system).
|
||||||
(This isn't recommended anyway).
|
(This isn't recommended anyway).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `@font-face` importing fonts from the `src/` dir
|
## `@font-face` importing fonts from the `src/` dir
|
||||||
|
|
||||||
If you are building the theme with [--external-assets](#enable-loading-in-a-blink-of-a-eye-of-login-pages-)
|
If you are building the theme with [--external-assets](#enable-loading-in-a-blink-of-a-eye-of-login-pages-)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "0.4.2",
|
"version": "1.0.1",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Keycloak theme generator for Reacts app",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -14,7 +14,8 @@ import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
|||||||
export const pageIds = [
|
export const pageIds = [
|
||||||
"login.ftl", "register.ftl", "info.ftl",
|
"login.ftl", "register.ftl", "info.ftl",
|
||||||
"error.ftl", "login-reset-password.ftl",
|
"error.ftl", "login-reset-password.ftl",
|
||||||
"login-verify-email.ftl", "terms.ftl"
|
"login-verify-email.ftl", "terms.ftl",
|
||||||
|
"login-otp.ftl"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type PageId = typeof pageIds[number];
|
export type PageId = typeof pageIds[number];
|
||||||
|
37
src/bin/build-keycloak-theme/generateFtl/login-otp.ftl
Normal file
37
src/bin/build-keycloak-theme/generateFtl/login-otp.ftl
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<script>const _=
|
||||||
|
{
|
||||||
|
"otpLogin": {
|
||||||
|
"userOtpCredentials": (function(){
|
||||||
|
|
||||||
|
var out = [];
|
||||||
|
|
||||||
|
<#attempt>
|
||||||
|
<#list otpLogin.userOtpCredentials as otpCredential>
|
||||||
|
out.push({
|
||||||
|
"id": (function (){
|
||||||
|
|
||||||
|
<#attempt>
|
||||||
|
return "${otpCredential.id}";
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
})(),
|
||||||
|
"userLabel": (function (){
|
||||||
|
|
||||||
|
<#attempt>
|
||||||
|
return "${otpCredential.userLabel}";
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
})()
|
||||||
|
});
|
||||||
|
</#list>
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
return out;
|
||||||
|
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -170,7 +170,7 @@
|
|||||||
out.push((function (){
|
out.push((function (){
|
||||||
|
|
||||||
<#attempt>
|
<#attempt>
|
||||||
return "${authorizedMailDomains}";
|
return "${authorizedMailDomain}";
|
||||||
<#recover>
|
<#recover>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import { Error } from "./Error";
|
|||||||
import { LoginResetPassword } from "./LoginResetPassword";
|
import { LoginResetPassword } from "./LoginResetPassword";
|
||||||
import { LoginVerifyEmail } from "./LoginVerifyEmail";
|
import { LoginVerifyEmail } from "./LoginVerifyEmail";
|
||||||
import { Terms } from "./Terms";
|
import { Terms } from "./Terms";
|
||||||
|
import { LoginOtp } from "./LoginOtp";
|
||||||
|
|
||||||
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContext; } & KcProps ) => {
|
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContext; } & KcProps ) => {
|
||||||
switch (kcContext.pageId) {
|
switch (kcContext.pageId) {
|
||||||
@ -19,5 +20,6 @@ export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContext; } &
|
|||||||
case "login-reset-password.ftl": return <LoginResetPassword {...{ kcContext, ...props }} />;
|
case "login-reset-password.ftl": return <LoginResetPassword {...{ kcContext, ...props }} />;
|
||||||
case "login-verify-email.ftl": return <LoginVerifyEmail {...{ kcContext, ...props }} />;
|
case "login-verify-email.ftl": return <LoginVerifyEmail {...{ kcContext, ...props }} />;
|
||||||
case "terms.ftl": return <Terms {...{ kcContext, ...props }}/>;
|
case "terms.ftl": return <Terms {...{ kcContext, ...props }}/>;
|
||||||
|
case "login-otp.ftl": return <LoginOtp {...{ kcContext, ...props }}/>;
|
||||||
}
|
}
|
||||||
});
|
});
|
145
src/lib/components/LoginOtp.tsx
Normal file
145
src/lib/components/LoginOtp.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import { useEffect, memo } from "react";
|
||||||
|
import { Template } from "./Template";
|
||||||
|
import type { KcProps } from "./KcProps";
|
||||||
|
import type { KcContext } from "../KcContext";
|
||||||
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
|
import { appendHead } from "../tools/appendHead";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { cx } from "tss-react";
|
||||||
|
|
||||||
|
export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContext.LoginOtp; } & KcProps) => {
|
||||||
|
|
||||||
|
const { otpLogin, url } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = useKcMessage();
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
|
||||||
|
let isCleanedUp = false;
|
||||||
|
|
||||||
|
appendHead({
|
||||||
|
"type": "javascript",
|
||||||
|
"src": pathJoin(
|
||||||
|
kcContext.url.resourcesCommonPath,
|
||||||
|
"node_modules/jquery/dist/jquery.min.js"
|
||||||
|
)
|
||||||
|
}).then(() => {
|
||||||
|
|
||||||
|
if (isCleanedUp) return;
|
||||||
|
|
||||||
|
evaluateInlineScript();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => { isCleanedUp = true };
|
||||||
|
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, ...props }}
|
||||||
|
headerNode={msg("doLogIn")}
|
||||||
|
formNode={
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="kc-otp-login-form"
|
||||||
|
className={cx(props.kcFormClass)}
|
||||||
|
action={url.loginAction}
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
otpLogin.userOtpCredentials.length > 1 &&
|
||||||
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
|
{
|
||||||
|
otpLogin.userOtpCredentials.map(otpCredential =>
|
||||||
|
<div className={cx(props.kcSelectOTPListClass)}>
|
||||||
|
<input type="hidden" value="${otpCredential.id}" />
|
||||||
|
<div className={cx(props.kcSelectOTPListItemClass)}>
|
||||||
|
<span className={cx(props.kcAuthenticatorOtpCircleClass)} />
|
||||||
|
<h2 className={cx(props.kcSelectOTPItemHeadingClass)}>
|
||||||
|
{otpCredential.userLabel}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor="otp" className={cx(props.kcLabelClass)}>
|
||||||
|
{msg("loginOtpOneTime")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
id="otp"
|
||||||
|
name="otp"
|
||||||
|
autoComplete="off"
|
||||||
|
type="text"
|
||||||
|
className={cx(props.kcInputClass)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||||
|
<div className={cx(props.kcFormOptionsWrapperClass)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||||
|
<input
|
||||||
|
className={cx(
|
||||||
|
props.kcButtonClass,
|
||||||
|
props.kcButtonPrimaryClass,
|
||||||
|
props.kcButtonBlockClass,
|
||||||
|
props.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
name="login"
|
||||||
|
id="kc-login"
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doLogIn")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form >
|
||||||
|
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
declare const $: any;
|
||||||
|
|
||||||
|
function evaluateInlineScript() {
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
// Card Single Select
|
||||||
|
$('.card-pf-view-single-select').click(function (this: any) {
|
||||||
|
if ($(this).hasClass('active')) { $(this).removeClass('active'); $(this).children().removeAttr('name'); }
|
||||||
|
else {
|
||||||
|
$('.card-pf-view-single-select').removeClass('active');
|
||||||
|
$('.card-pf-view-single-select').children().removeAttr('name');
|
||||||
|
$(this).addClass('active'); $(this).children().attr('name', 'selectedCredentialId');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var defaultCred = $('.card-pf-view-single-select')[0];
|
||||||
|
if (defaultCred) {
|
||||||
|
defaultCred.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
@ -17,7 +17,7 @@ type ExtractAfterStartingWith<Prefix extends string, StrEnum> =
|
|||||||
export type KcContext =
|
export type KcContext =
|
||||||
KcContext.Login | KcContext.Register | KcContext.Info |
|
KcContext.Login | KcContext.Register | KcContext.Info |
|
||||||
KcContext.Error | KcContext.LoginResetPassword | KcContext.LoginVerifyEmail |
|
KcContext.Error | KcContext.LoginResetPassword | KcContext.LoginVerifyEmail |
|
||||||
KcContext.Terms;
|
KcContext.Terms | KcContext.LoginOtp;
|
||||||
|
|
||||||
export declare namespace KcContext {
|
export declare namespace KcContext {
|
||||||
|
|
||||||
@ -124,6 +124,10 @@ export declare namespace KcContext {
|
|||||||
passwordRequired: boolean;
|
passwordRequired: boolean;
|
||||||
recaptchaRequired: boolean;
|
recaptchaRequired: boolean;
|
||||||
recaptchaSiteKey?: string;
|
recaptchaSiteKey?: string;
|
||||||
|
/**
|
||||||
|
* Defined when you use the keycloak-mail-whitelisting keycloak plugin
|
||||||
|
* (https://github.com/micedre/keycloak-mail-whitelisting)
|
||||||
|
*/
|
||||||
authorizedMailDomains?: string[];
|
authorizedMailDomains?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -161,6 +165,13 @@ export declare namespace KcContext {
|
|||||||
pageId: "terms.ftl";
|
pageId: "terms.ftl";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LoginOtp = Common & {
|
||||||
|
pageId: "login-otp.ftl";
|
||||||
|
otpLogin: {
|
||||||
|
userOtpCredentials: { id: string; userLabel: string; }[];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doExtends<KcContext["pageId"], PageId>();
|
doExtends<KcContext["pageId"], PageId>();
|
||||||
|
@ -211,3 +211,20 @@ export const kcTermsContext: KcContext.Terms = {
|
|||||||
"pageId": "terms.ftl"
|
"pageId": "terms.ftl"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const kcLoginOtpContext: KcContext.LoginOtp = {
|
||||||
|
...kcCommonContext,
|
||||||
|
"pageId": "login-otp.ftl",
|
||||||
|
"otpLogin": {
|
||||||
|
"userOtpCredentials": [
|
||||||
|
{
|
||||||
|
"id": "id1",
|
||||||
|
"userLabel": "label1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id2",
|
||||||
|
"userLabel": "label2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user