Generate kcContext automatically 🚀

This commit is contained in:
garronej 2021-06-22 12:37:21 +02:00
parent 0dacf2fe30
commit 2cd266caff
14 changed files with 190 additions and 934 deletions

View File

@ -271,6 +271,10 @@ Then to load your own therms of services using [like this](https://github.com/ga
# Some pages still have the default theme. Why? # Some pages still have the default theme. Why?
**NEW in v1.2 it is now much more easy to add support for custom pages since the
Keycloak context is now automatically converted into a JavaScript object (kcContext).
In v2 (coming soon) it won't be required to fork for adding support for custom pages.**
This project only support the most common user facing pages of Keycloak login. This project only support the most common user facing pages of Keycloak login.
[Here](https://user-images.githubusercontent.com/6702424/116787906-227fe700-aaa7-11eb-92ee-22e7673717c2.png) is the complete list of pages (you get them after running `yarn test`) [Here](https://user-images.githubusercontent.com/6702424/116787906-227fe700-aaa7-11eb-92ee-22e7673717c2.png) is the complete list of pages (you get them after running `yarn test`)
and [here](https://github.com/InseeFrLab/keycloakify/tree/main/src/lib/components) are the pages currently implemented by this module. and [here](https://github.com/InseeFrLab/keycloakify/tree/main/src/lib/components) are the pages currently implemented by this module.

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "1.1.6", "version": "1.2.0",
"description": "Keycloak theme generator for Reacts app", "description": "Keycloak theme generator for Reacts app",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,26 +0,0 @@
var es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
var unes = {
'&': '&',
'&': '&',
'&lt;': '<',
'&#60;': '<',
'&gt;': '>',
'&#62;': '>',
'&apos;': "'",
'&#39;': "'",
'&quot;': '"',
'&#34;': '"'
};
var cape = function (m) { return unes[m]; };
Object.defineProperty(
String,
"htmlUnescape",
{
"value": function (un) {
return String.prototype.replace.call(un, es, cape);
}
}
);

View File

@ -1,263 +1,194 @@
<script>const _= <script>const _=
{ <#macro objectToJson object depth>
"url": { <@compress>
"loginAction": (function (){
<#local isHash = false>
<#attempt>
<#local isHash = object?is_hash || object?is_hash_ex>
<#recover>
/* can't evaluate if object is hash */
undefined
<#return>
</#attempt>
<#if isHash>
<#local keys = "">
<#attempt> <#attempt>
return "${url.loginAction?no_esc}"; <#local keys = object?keys>
<#recover> <#recover>
/* can't list keys of object */
undefined
<#return>
</#attempt> </#attempt>
})(), {${'\n'}
"resourcesPath": (function (){
/* <#list keys as key> ${key} </#list> */
<#list keys as key>
<#if key == "class">
/* skipping "class" property of object */
<#continue>
</#if>
<#local value = "">
<#attempt> <#attempt>
return "${url.resourcesPath?no_esc}"; <#local value = object[key]>
<#recover> <#recover>
/* couldn't dereference ${key} of object */
<#continue>
</#attempt> </#attempt>
})(), <#if depth gt 4>
"resourcesCommonPath": (function (){ /* Avoid calling recustively too many times depth: ${depth}, key: ${key} */
<#continue>
</#if>
<#attempt> "${key}": <@objectToJson object=value depth=depth+1/>,
return "${url.resourcesCommonPath?no_esc}";
<#recover>
</#attempt>
})(),
"loginRestartFlowUrl": (function (){
<#attempt>
return "${url.loginRestartFlowUrl?no_esc}";
<#recover>
</#attempt>
})(),
"loginUrl": (function (){
<#attempt>
return "${url.loginUrl?no_esc}";
<#recover>
</#attempt>
})()
},
"realm": {
"displayName": (function (){
<#attempt>
return "${realm.displayName!''}" || undefined;
<#recover>
</#attempt>
})(),
"displayNameHtml": (function (){
<#attempt>
return "${realm.displayNameHtml!''}" || undefined;
<#recover>
</#attempt>
})(),
"internationalizationEnabled": (function (){
<#attempt>
return ${realm.internationalizationEnabled?c};
<#recover>
</#attempt>
})(),
"registrationEmailAsUsername": (function (){
<#attempt>
return ${realm.registrationEmailAsUsername?c};
<#recover>
</#attempt>
})()
},
"locale": (function (){
<#attempt>
<#if realm.internationalizationEnabled>
return {
"supported": (function(){
var out= [];
<#attempt>
<#list locale.supported as lng>
out.push({
"url": (function (){
<#attempt>
return "${lng.url?no_esc}";
<#recover>
</#attempt>
})(),
"label": (function (){
<#attempt>
return "${lng.label}";
<#recover>
</#attempt>
})(),
"languageTag": (function (){
<#attempt>
return "${lng.languageTag}";
<#recover>
</#attempt>
})()
});
</#list> </#list>
<#recover>
</#attempt>
return out; }${'\n'}
})(), <#return>
"current": (function (){
<#attempt>
return "${locale.current}";
<#recover>
</#attempt>
})()
};
</#if> </#if>
<#local isMethod = "">
<#attempt>
<#local isMethod = object?is_method>
<#recover> <#recover>
/* can't test if object is a method */
undefined
<#return>
</#attempt> </#attempt>
})(), <#if isMethod>
"auth": (function (){ undefined
<#return>
</#if>
<#local isBoolean = "">
<#attempt> <#attempt>
<#if auth?has_content> <#local isBoolean = object?is_boolean>
var out= {
"showUsername": (function (){
<#attempt>
return ${auth.showUsername()?c};
<#recover> <#recover>
/* can't test if object is a boolean */
undefined
<#return>
</#attempt> </#attempt>
})(), <#if isBoolean>
"showResetCredentials": (function (){ ${object?c}
<#return>
</#if>
<#local isEnumerable = "">
<#attempt> <#attempt>
return ${auth.showResetCredentials()?c}; <#local isEnumerable = object?is_enumerable>
<#recover> <#recover>
/* can't test if object is enumerable */
undefined
<#return>
</#attempt> </#attempt>
})(), <#if isEnumerable>
"showTryAnotherWayLink": (function(){
[${'\n'}
<#list object as item>
<@objectToJson object=item depth=depth+1/>,
</#list>
]${'\n'}
<#return>
</#if>
<#attempt> <#attempt>
return ${auth.showTryAnotherWayLink()?c}; "${object?no_esc}"
<#recover> <#recover>
/* couldn't convert into string non hash, non method, non boolean, non enumerable object */
undefined;
<#return>
</#attempt> </#attempt>
})()
};
<#attempt> </@compress>
<#if auth.showUsername() && !auth.showResetCredentials()> </#macro>
Object.assign(
out, (()=>{
//Removing all the undefined
const obj = JSON.parse(JSON.stringify(<@objectToJson object=.data_model depth=0 />));
//Freemarker values that can't be automatically converted into a JavaScript object.
Object.deepAssign(
obj,
{ {
"attemptedUsername": (function (){ "messagesPerField": {
"printIfExists": function (key, x) {
switch(key){
case "userLabel": return (function (){
<#attempt> <#attempt>
return "${auth.attemptedUsername}"; return "${messagesPerField.printIfExists('userLabel','1')}" ? x : undefined;
<#recover> <#recover>
</#attempt> </#attempt>
})() })();
case "username": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('username','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "email": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('email','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "firstName": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('firstName','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "lastName": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('lastName','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "password": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('password','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "password-confirm": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('password-confirm','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
}
}
},
"msg": function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); },
} }
); );
</#if>
<#recover>
</#attempt>
return out; return obj;
</#if>
<#recover>
</#attempt>
})(),
"scripts": (function(){
var out = [];
<#attempt>
<#if scripts??>
<#attempt>
<#list scripts as script>
out.push((function (){
<#attempt>
return "${script}";
<#recover>
</#attempt>
})());
</#list>
<#recover>
</#attempt>
</#if>
<#recover>
</#attempt>
return out;
})(),
"message": (function (){
<#attempt>
<#if message?has_content>
return { 
"type": (function (){
<#attempt>
return "${message.type}";
<#recover>
</#attempt>
})(),
"summary": (function (){
<#attempt>
return String.htmlUnescape("${message.summary}");
<#recover>
</#attempt>
})() })()
};
</#if>
<#recover>
</#attempt>
})(),
"isAppInitiatedAction": (function (){
<#attempt>
<#if isAppInitiatedAction??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})()
}
</script> </script>

View File

@ -1,23 +0,0 @@
<script>const _=
{
"client": (function (){
<#attempt>
<#if client??>
return {
"baseUrl": (function (){
<#attempt>
return "${(client.baseUrl!'')?no_esc}" || undefined;
<#recover>
</#attempt>
})()
};
</#if>
<#recover>
</#attempt>
})()
}
</script>

View File

@ -26,17 +26,6 @@ function loadAdjacentFile(fileBasename: string) {
.toString("utf8"); .toString("utf8");
}; };
function loadFtlFile(ftlFileBasename: PageId | "common.ftl") {
try {
return loadAdjacentFile(ftlFileBasename)
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1];
} catch {
return "{}";
}
}
export function generateFtlFilesCodeFactory( export function generateFtlFilesCodeFactory(
params: { params: {
@ -100,8 +89,9 @@ export function generateFtlFilesCodeFactory(
); );
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later. //FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const ftlCommonPlaceholders = { const ftlPlaceholders = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadFtlFile("common.ftl"), '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadAdjacentFile("common.ftl")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
'<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->': '<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->':
[ [
'<#if scripts??>', '<#if scripts??>',
@ -126,23 +116,19 @@ export function generateFtlFilesCodeFactory(
'</style>', '</style>',
'' ''
]), ]),
...["Object.deepAssign.js", "String.htmlUnescape.js"].map(
fileBasename => [
"<script>", "<script>",
loadAdjacentFile(fileBasename), loadAdjacentFile("Object.deepAssign.js"),
"</script>" "</script>",
].join("\n")
),
'<script>', '<script>',
` window.${ftlValuesGlobalName}= Object.assign(`, ` window.${ftlValuesGlobalName}= Object.assign(`,
` {},`, ` {},`,
` ${objectKeys(ftlCommonPlaceholders)[0]}`, ` ${objectKeys(ftlPlaceholders)[0]}`,
' );', ' );',
'</script>', '</script>',
'', '',
pageSpecificCodePlaceholder, pageSpecificCodePlaceholder,
'', '',
objectKeys(ftlCommonPlaceholders)[1] objectKeys(ftlPlaceholders)[1]
].join("\n"), ].join("\n"),
); );
@ -158,11 +144,6 @@ export function generateFtlFilesCodeFactory(
const $ = cheerio.load(partiallyFixedIndexHtmlCode); const $ = cheerio.load(partiallyFixedIndexHtmlCode);
const ftlPlaceholders = {
'{ "x": "kxOlLqMeOed9sdLdIdOxd444" }': loadFtlFile(pageId),
...ftlCommonPlaceholders
};
let ftlCode = $.html() let ftlCode = $.html()
.replace( .replace(
pageSpecificCodePlaceholder, pageSpecificCodePlaceholder,
@ -172,10 +153,6 @@ export function generateFtlFilesCodeFactory(
` window.${ftlValuesGlobalName},`, ` window.${ftlValuesGlobalName},`,
` { "pageId": "${pageId}" }`, ` { "pageId": "${pageId}" }`,
' );', ' );',
` Object.deepAssign(`,
` window.${ftlValuesGlobalName},`,
` ${objectKeys(ftlPlaceholders)[0]}`,
' );',
'</script>' '</script>'
].join("\n") ].join("\n")
); );

View File

@ -1,82 +0,0 @@
<script>const _=
{
"messageHeader": (function (){
<#attempt>
return "${messageHeader!''}" || undefined;
<#recover>
</#attempt>
})(),
"requiredActions": (function (){
<#attempt>
<#if requiredActions??>
var out =[];
<#attempt>
<#list requiredActions>
<#attempt>
<#items as reqActionItem>
out.push((function (){
<#attempt>
return "${reqActionItem}";
<#recover>
</#attempt>
})());
</#items>
<#recover>
</#attempt>
</#list>
<#recover>
</#attempt>
return out;
</#if>
<#recover>
</#attempt>
})(),
"skipLink": (function (){
<#attempt>
<#if skipLink??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})(),
"pageRedirectUri": (function (){
<#attempt>
return "${(pageRedirectUri!'')?no_esc}" || undefined;
<#recover>
</#attempt>
})(),
"actionUri": (function (){
<#attempt>
return "${(actionUri!'')?no_esc}" || undefined;
<#recover>
</#attempt>
})(),
"client": {
"baseUrl": (function(){
<#attempt>
return "${(client.baseUrl!'')?no_esc}" || undefined;
<#recover>
</#attempt>
})()
}
}
</script>

View File

@ -1,11 +0,0 @@
<script>const _=
{
"idpAlias": (function (){
<#attempt>
return "${idpAlias}";
<#recover>
</#attempt>
return "";
})()
}
</script>

View File

@ -1,37 +0,0 @@
<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>

View File

@ -1,14 +0,0 @@
<script>const _=
{
"realm": {
"loginWithEmailAllowed": (function (){
<#attempt>
return ${realm.loginWithEmailAllowed?c};
<#recover>
</#attempt>
})()
}
}
</script>

View File

@ -1,67 +0,0 @@
<script>const _=
{
"user": {
"editUsernameAllowed": (function (){
<#attempt>
return ${user.editUsernameAllowed?c};
<#recover>
</#attempt>
})(),
"username": (function (){
<#attempt>
return "${user.username!''}" || undefined;
<#recover>
</#attempt>
})(),
"email": (function (){
<#attempt>
return "${user.email!''}" || undefined;
<#recover>
</#attempt>
})(),
"firstName": (function (){
<#attempt>
return "${user.firstName!''}" || undefined;
<#recover>
</#attempt>
})(),
"lastName": (function (){
<#attempt>
return "${user.lastName!''}" || undefined;
<#recover>
</#attempt>
})()
},
"messagesPerField": {
"printIfExists": function (key, x) {
switch(key){
case "username": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('username','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "email": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('email','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "firstName": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('firstName','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "lastName": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('lastName','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
}
}
},
}
</script>

View File

@ -1,160 +0,0 @@
<script>const _=
{
"url": {
"loginResetCredentialsUrl": (function (){
<#attempt>
return "${url.loginResetCredentialsUrl?no_esc}";
<#recover>
</#attempt>
})(),
"registrationUrl": (function (){
<#attempt>
return "${url.registrationUrl?no_esc}";
<#recover>
</#attempt>
})()
},
"realm": {
"loginWithEmailAllowed": (function(){
<#attempt>
return ${realm.loginWithEmailAllowed?c};
<#recover>
</#attempt>
})(),
"rememberMe": (function (){
<#attempt>
return ${realm.rememberMe?c};
<#recover>
</#attempt>
})(),
"password": (function (){
<#attempt>
return ${realm.password?c};
<#recover>
</#attempt>
})(),
"resetPasswordAllowed": (function (){
<#attempt>
return ${realm.resetPasswordAllowed?c};
<#recover>
</#attempt>
})(),
"registrationAllowed": (function (){
<#attempt>
return ${realm.registrationAllowed?c};
<#recover>
</#attempt>
})()
},
"auth": (function (){
<#attempt>
<#if auth?has_content>
return {
"selectedCredential": (function (){
<#attempt>
return "${auth.selectedCredential!''}" || undefined;
<#recover>
</#attempt>
})()
};
</#if>
<#recover>
</#attempt>
})(),
"social": {
"displayInfo": (function (){
<#attempt>
return ${social.displayInfo?c};
<#recover>
</#attempt>
})(),
"providers": (()=>{
<#attempt>
<#if social.providers??>
var out= [];
<#attempt>
<#list social.providers as p>
out.push({
"loginUrl": (function (){
<#attempt>
return "${p.loginUrl?no_esc}";
<#recover>
</#attempt>
})(),
"alias": (function (){
<#attempt>
return "${p.alias}";
<#recover>
</#attempt>
})(),
"providerId": (function (){
<#attempt>
return "${p.providerId}";
<#recover>
</#attempt>
})(),
"displayName": (function (){
<#attempt>
return "${p.displayName}";
<#recover>
</#attempt>
})()
});
</#list>
<#recover>
</#attempt>
return out;
</#if>
<#recover>
</#attempt>
})()
},
"usernameEditDisabled": (function () {
<#attempt>
<#if usernameEditDisabled??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})(),
"login": {
"username": (function (){
<#attempt>
return "${login.username!''}" || undefined;
<#recover>
</#attempt>
})(),
"rememberMe": (function (){
<#attempt>
<#if login.rememberMe??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})()
},
"registrationDisabled": (function (){
<#attempt>
<#if registrationDisabled??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})()
}
</script>

View File

@ -1,243 +0,0 @@
<script>const _=
{
"url": {
"registrationAction": (function (){
<#attempt>
return "${url.registrationAction?no_esc}";
<#recover>
</#attempt>
})()
},
"messagesPerField": {
"printIfExists": function (key, x) {
switch(key){
case "userLabel": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('userLabel','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "username": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('username','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "email": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('email','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "firstName": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('firstName','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "lastName": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('lastName','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "password": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('password','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "password-confirm": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('password-confirm','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
}
}
},
"register": {
"formData": {
"firstName": (function (){
<#attempt>
return "${register.formData.firstName!''}" || undefined;
<#recover>
</#attempt>
})(),
"displayName": (function (){
<#attempt>
return "${register.formData.displayName!''}" || undefined;
<#recover>
</#attempt>
})(),
"lastName": (function (){
<#attempt>
return "${register.formData.lastName!''}" || undefined;
<#recover>
</#attempt>
})(),
"email": (function(){
<#attempt>
return "${register.formData.email!''}" || undefined;
<#recover>
</#attempt>
})(),
"username": (function (){
<#attempt>
return "${register.formData.username!''}" || undefined;
<#recover>
</#attempt>
})()
}
},
"passwordRequired": (function (){
<#attempt>
<#if passwordRequired??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})(),
"recaptchaRequired": (function (){
<#attempt>
<#if passwordRequired??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})(),
"recaptchaSiteKey": (function (){
<#attempt>
return "${recaptchaSiteKey!''}" || undefined;
<#recover>
</#attempt>
})(),
"authorizedMailDomains": (function (){
<#attempt>
return "${authorizedMailDomains!''}" || undefined;
<#recover>
</#attempt>
})(),
"authorizedMailDomains": (function(){
var out = undefined;
<#attempt>
<#if authorizedMailDomains??>
out = [];
<#attempt>
<#list authorizedMailDomains as authorizedMailDomain>
out.push((function (){
<#attempt>
return "${authorizedMailDomain}";
<#recover>
</#attempt>
})());
</#list>
<#recover>
</#attempt>
</#if>
<#recover>
</#attempt>
return out;
})(),
"social": {
"displayInfo": (function (){
<#attempt>
return ${social.displayInfo?c};
<#recover>
</#attempt>
})(),
"providers": (()=>{
<#attempt>
<#if social.providers??>
var out= [];
<#attempt>
<#list social.providers as p>
out.push({
"loginUrl": (function (){
<#attempt>
return "${p.loginUrl?no_esc}";
<#recover>
</#attempt>
})(),
"alias": (function (){
<#attempt>
return "${p.alias}";
<#recover>
</#attempt>
})(),
"providerId": (function (){
<#attempt>
return "${p.providerId}";
<#recover>
</#attempt>
})(),
"displayName": (function (){
<#attempt>
return "${p.displayName}";
<#recover>
</#attempt>
})()
});
</#list>
<#recover>
</#attempt>
return out;
</#if>
<#recover>
</#attempt>
})()
}
}
</script>

View File

@ -9,6 +9,13 @@ import ReactMarkdown from "react-markdown";
export type MessageKey = keyof typeof kcMessages["en"]; export type MessageKey = keyof typeof kcMessages["en"];
/**
* When the language is switched the page is reloaded, this may appear
* as a bug as you might notice that the language successfully switch before
* reload.
* However we need to tell Keycloak that the user have changed the language
* during login so we can retrieve the "local" field of the JWT encoded accessToken.
*/
export function useKcMessage() { export function useKcMessage() {
const { kcLanguageTag } = useKcLanguageTag(); const { kcLanguageTag } = useKcLanguageTag();