Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into InseeFrLab-main

This commit is contained in:
Ann Bystrova
2022-01-19 01:13:36 +03:00
19 changed files with 718 additions and 274 deletions

View File

@ -4,6 +4,7 @@ import { join as pathJoin, relative as pathRelative, basename as pathBasename }
import * as child_process from "child_process";
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
import { URL } from "url";
import * as fs from "fs";
type ParsedPackageJson = {
name: string;
@ -41,7 +42,17 @@ export function main() {
const url = (() => {
const { homepage } = parsedPackageJson;
return homepage === undefined ? undefined : new URL(homepage);
if (homepage !== undefined) {
return new URL(homepage);
}
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
if (fs.existsSync(cnameFilePath)) {
return new URL(`https://${fs.readFileSync(cnameFilePath).toString("utf8").replace(/\s+$/, "")}`);
}
return undefined;
})();
return {
@ -121,11 +132,14 @@ export function main() {
"",
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, containerLaunchScriptBasename))} 👈`,
"",
'To enable the theme within keycloak log into the admin console ( 👉 http://localhost:8080 username: admin, password: admin 👈), create a realm (called "myrealm" for example),',
`go to your realm settings, click on the theme tab then select ${themeName}.`,
`More details: https://www.keycloak.org/getting-started/getting-started-docker`,
"Once your container is up and running: ",
"- Log into the admin console 👉 http://localhost:8080 username: admin, password: admin 👈",
'- Create a realm named "myrealm"',
'- Create a client with id "myclient" and root url: "https://www.keycloak.org/app/"',
`- Select Login Theme: ${themeName} (don't forget to save at the bottom of the page)`,
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
"",
"Once your container is up and configured 👉 http://localhost:8080/auth/realms/myrealm/account 👈",
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
"",
].join("\n"),
);

View File

@ -1,28 +0,0 @@
Object.defineProperty(
Object,
"deepAssign",
{
"value": function callee(target, source) {
Object.keys(source).forEach(function (key) {
var value = source[key];
if (target[key] === undefined) {
target[key] = value;
return;
}
if (value instanceof Object) {
if (value instanceof Array) {
value.forEach(function (entry) {
target[key].push(entry);
});
return;
}
callee(target[key], value);
return;
}
target[key] = value;
});
return target;
}
}
);

View File

@ -0,0 +1,290 @@
<script>const _=
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
(()=>{
const out =
${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["messagesPerField"]= {
<#assign fieldNames = [
"global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm",
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"
]>
<#attempt>
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
<#list profile.attributes as attribute>
<#if fieldNames?seq_contains(attribute.name)>
<#continue>
</#if>
<#assign fieldNames += [attribute.name]>
</#list>
</#if>
<#recover>
</#attempt>
"printIfExists": function (fieldName, x) {
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
return "${messagesPerField.printIfExists(fieldName,'1')}" ? x : undefined;
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
},
"existsError": function (fieldName) {
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
},
"get": function (fieldName) {
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
<#if messagesPerField.existsError('${fieldName}')>
return "${messagesPerField.get('${fieldName}')?no_esc}";
</#if>
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
},
"exists": function (fieldName) {
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
}
};
out["pageId"] = "${pageId}";
return out;
})()
<#function ftl_object_to_js_code_declaring_an_object object path>
<#local isHash = "">
<#attempt>
<#local isHash = object?is_hash || object?is_hash_ex>
<#recover>
<#return "ABORT: Can't evaluate if " + path?join(".") + " is hash">
</#attempt>
<#if isHash>
<#if path?size gt 10>
<#return "ABORT: Too many recursive calls">
</#if>
<#local keys = "">
<#attempt>
<#local keys = object?keys>
<#recover>
<#return "ABORT: We can't list keys on this object">
</#attempt>
<#local out_seq = []>
<#list keys as key>
<#if ["class","declaredConstructors","superclass","declaringClass" ]?seq_contains(key) >
<#continue>
</#if>
<#if
(
["loginUpdatePasswordUrl", "loginUpdateProfileUrl", "loginUsernameReminderUrl", "loginUpdateTotpUrl"]?seq_contains(key) &&
are_same_path(path, ["url"])
) || (
key == "updateProfileCtx" &&
are_same_path(path, [])
) || (
<#-- https://github.com/InseeFrLab/keycloakify/pull/65#issuecomment-991896344 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
pageId == "saml-post-form.ftl"
)
>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#if key == "attemptedUsername" && are_same_path(path, ["auth"])>
<#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#continue>
</#if>
<#recover>
</#attempt>
</#if>
<#attempt>
<#if !object[key]??>
<#continue>
</#if>
<#recover>
<#local out_seq += ["/*Couldn't test if '" + key + "' is available on this object*/"]>
<#continue>
</#attempt>
<#local propertyValue = "">
<#attempt>
<#local propertyValue = object[key]>
<#recover>
<#local out_seq += ["/*Couldn't dereference '" + key + "' on this object*/"]>
<#continue>
</#attempt>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(propertyValue, path + [ key ])>
<#if rec_out?starts_with("ABORT:")>
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local out_seq += ["/*" + key + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local out_seq += ['"' + key + '": ' + rec_out + ","]>
</#list>
<#return (["{"] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "}"])?join("\n")>
</#if>
<#local isMethod = "">
<#attempt>
<#local isMethod = object?is_method>
<#recover>
<#return "ABORT: Can't test if it'sa method.">
</#attempt>
<#if isMethod>
<#return "ABORT: It's a method">
</#if>
<#local isBoolean = "">
<#attempt>
<#local isBoolean = object?is_boolean>
<#recover>
<#return "ABORT: Can't test if it's a boolean">
</#attempt>
<#if isBoolean>
<#return object?c>
</#if>
<#local isEnumerable = "">
<#attempt>
<#local isEnumerable = object?is_enumerable>
<#recover>
<#return "ABORT: Can't test if it's an enumerable">
</#attempt>
<#if isEnumerable>
<#local out_seq = []>
<#local i = 0>
<#list object as array_item>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
<#local i = i + 1>
<#if rec_out?starts_with("ABORT:")>
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local out_seq += ["/*" + i?string + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local out_seq += [rec_out + ","]>
</#list>
<#return (["["] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "]"])?join("\n")>
</#if>
<#attempt>
<#return '"' + object?js_string + '"'>;
<#recover>
</#attempt>
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
</#function>
<#function are_same_path path searchedPath>
<#if path?size != path?size>
<#return false>
</#if>
<#local i=0>
<#list path as property>
<#local searchedProperty=searchedPath[i]>
<#if searchedProperty?is_string && searchedProperty == "*">
<#continue>
</#if>
<#if searchedProperty?is_string && !property?is_string>
<#return false>
</#if>
<#if searchedProperty?is_number && !property?is_number>
<#return false>
</#if>
<#if searchedProperty?string != property?string>
<#return false>
</#if>
<#local i+= 1>
</#list>
<#return true>
</#function>
</script>

View File

@ -16,15 +16,13 @@ export const pageIds = [
"terms.ftl",
"login-otp.ftl",
"login-update-profile.ftl",
"login-update-password.ftl",
"login-idp-link-confirm.ftl",
"login-page-expired.ftl",
] as const;
export type PageId = typeof pageIds[number];
function loadAdjacentFile(fileBasename: string) {
return fs.readFileSync(pathJoin(__dirname, fileBasename)).toString("utf8");
}
export function generateFtlFilesCodeFactory(params: {
cssGlobalsToDefine: Record<string, string>;
indexHtmlCode: string;
@ -77,8 +75,11 @@ export function generateFtlFilesCodeFactory(params: {
);
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const ftlPlaceholders = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadAdjacentFile("common.ftl").match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
const replaceValueBySearchValue = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",
@ -88,8 +89,6 @@ export function generateFtlFilesCodeFactory(params: {
].join("\n"),
};
const pageSpecificCodePlaceholder = "<!-- dIddLqMeOedErIdLsPdNdI9dSl42sw -->";
$("head").prepend(
[
...(Object.keys(cssGlobalsToDefine).length === 0
@ -105,18 +104,10 @@ export function generateFtlFilesCodeFactory(params: {
"",
]),
"<script>",
loadAdjacentFile("Object.deepAssign.js"),
"</script>",
"<script>",
` window.${ftlValuesGlobalName}= Object.assign(`,
` {},`,
` ${objectKeys(ftlPlaceholders)[0]}`,
" );",
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"",
pageSpecificCodePlaceholder,
"",
objectKeys(ftlPlaceholders)[1],
objectKeys(replaceValueBySearchValue)[1],
].join("\n"),
);
@ -129,21 +120,13 @@ export function generateFtlFilesCodeFactory(params: {
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
let ftlCode = $.html().replace(
pageSpecificCodePlaceholder,
[
"<script>",
` Object.deepAssign(`,
` window.${ftlValuesGlobalName},`,
` { "pageId": "${pageId}" }`,
" );",
"</script>",
].join("\n"),
);
let ftlCode = $.html();
objectKeys(ftlPlaceholders).forEach(
id => (ftlCode = ftlCode.replace(id, ftlPlaceholders[id]).replace("ftl_template_for_replacement", pageId)),
);
Object.entries({
...replaceValueBySearchValue,
//If updated, don't forget to change in the ftl script as well.
"PAGE_ID_xIgLsPgGId9D8e": pageId,
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
return { ftlCode };
}

View File

@ -37,7 +37,7 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; urlPath
const { cssCode, urlPathname, urlOrigin } = params;
const fixedCssCode = cssCode.replace(
urlPathname === "/" ? /url\(\/([^/][^)]+)\)/g : new RegExp(`url\\(${urlPathname}([^)]+)\\)`, "g"),
urlPathname === "/" ? /url\(["']?\/([^/][^)"']+)["']?\)/g : new RegExp(`url\\(["']?${urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) => `url(${urlOrigin === undefined ? "${url.resourcesPath}/build/" + group : params.urlOrigin + urlPathname + group})`,
);
@ -52,7 +52,7 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(\/[^/][^)]+\)[^;}]*/g) ?? []).forEach(
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match),
);