Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
9f52eb8123 | |||
744b198fb4 | |||
15eab797c3 | |||
8ff86b1e29 | |||
e1b8760ee3 | |||
bd0d890b2c | |||
2a2118d769 | |||
9839b64650 | |||
2bf55e12f9 | |||
2249fa9232 | |||
f673a65304 | |||
0163459ad6 | |||
b21123cc9d | |||
7800d125b2 | |||
89ea648f18 | |||
ab7ac3c2d0 | |||
b16319d962 | |||
f8012d5dfb | |||
45a2015597 |
21
CHANGELOG.md
21
CHANGELOG.md
@ -1,3 +1,24 @@
|
||||
### **1.0.2** (2021-05-01)
|
||||
|
||||
|
||||
|
||||
### **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)
|
||||
|
||||
- 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)
|
||||
- [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)
|
||||
- [Some pages still have the default theme. Why?](#some-pages-still-have-the-default-theme-why)
|
||||
- [GitHub Actions](#github-actions)
|
||||
- [Requirements](#requirements)
|
||||
- [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)
|
||||
|
||||
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
|
||||
|
||||
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).
|
||||
|
||||
# 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
|
||||
|
||||

|
||||
@ -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).
|
||||
(This isn't recommended anyway).
|
||||
|
||||
|
||||
|
||||
## `@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-)
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "0.4.2",
|
||||
"version": "1.0.2",
|
||||
"description": "Keycloak theme generator for Reacts app",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -14,7 +14,8 @@ import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||
export const pageIds = [
|
||||
"login.ftl", "register.ftl", "info.ftl",
|
||||
"error.ftl", "login-reset-password.ftl",
|
||||
"login-verify-email.ftl", "terms.ftl"
|
||||
"login-verify-email.ftl", "terms.ftl",
|
||||
"login-otp.ftl"
|
||||
] as const;
|
||||
|
||||
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 (){
|
||||
|
||||
<#attempt>
|
||||
return "${authorizedMailDomains}";
|
||||
return "${authorizedMailDomain}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
|
@ -9,6 +9,7 @@ import { Error } from "./Error";
|
||||
import { LoginResetPassword } from "./LoginResetPassword";
|
||||
import { LoginVerifyEmail } from "./LoginVerifyEmail";
|
||||
import { Terms } from "./Terms";
|
||||
import { LoginOtp } from "./LoginOtp";
|
||||
|
||||
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContext; } & KcProps ) => {
|
||||
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-verify-email.ftl": return <LoginVerifyEmail {...{ 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 key={otpCredential.id} 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 =
|
||||
KcContext.Login | KcContext.Register | KcContext.Info |
|
||||
KcContext.Error | KcContext.LoginResetPassword | KcContext.LoginVerifyEmail |
|
||||
KcContext.Terms;
|
||||
KcContext.Terms | KcContext.LoginOtp;
|
||||
|
||||
export declare namespace KcContext {
|
||||
|
||||
@ -124,6 +124,10 @@ export declare namespace KcContext {
|
||||
passwordRequired: boolean;
|
||||
recaptchaRequired: boolean;
|
||||
recaptchaSiteKey?: string;
|
||||
/**
|
||||
* Defined when you use the keycloak-mail-whitelisting keycloak plugin
|
||||
* (https://github.com/micedre/keycloak-mail-whitelisting)
|
||||
*/
|
||||
authorizedMailDomains?: string[];
|
||||
};
|
||||
|
||||
@ -161,6 +165,13 @@ export declare namespace KcContext {
|
||||
pageId: "terms.ftl";
|
||||
};
|
||||
|
||||
export type LoginOtp = Common & {
|
||||
pageId: "login-otp.ftl";
|
||||
otpLogin: {
|
||||
userOtpCredentials: { id: string; userLabel: string; }[];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
doExtends<KcContext["pageId"], PageId>();
|
||||
|
@ -211,3 +211,20 @@ export const kcTermsContext: KcContext.Terms = {
|
||||
"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