Split kcContext among pages

This commit is contained in:
Joseph Garrone
2021-03-04 21:14:54 +01:00
parent 546c74aa28
commit 624409434a
12 changed files with 428 additions and 455 deletions

View File

@ -1,214 +0,0 @@
<script>const _=
{
"url": {
"loginAction": "${url.loginAction}",
"resourcesPath": "${url.resourcesPath}",
"resourcesCommonPath": "${url.resourcesCommonPath}",
"loginRestartFlowUrl": "${url.loginRestartFlowUrl}",
"loginResetCredentialsUrl": "${url.loginResetCredentialsUrl}",
"registrationUrl": "${url.registrationUrl}",
"registrationAction": "${url.registrationUrl}",
"loginUrl": "${url.loginUrl}"
},
"realm": {
"displayName": "${realm.displayName!''}" || undefined,
"displayNameHtml": "${realm.displayNameHtml!''}" || undefined,
"internationalizationEnabled": ${realm.internationalizationEnabled?c},
"password": ${realm.password?c},
"loginWithEmailAllowed": ${realm.loginWithEmailAllowed?c},
"registrationEmailAsUsername": ${realm.registrationEmailAsUsername?c},
"rememberMe": ${realm.rememberMe?c},
"resetPasswordAllowed": ${realm.resetPasswordAllowed?c}
},
"locale": (function (){
<#if realm.internationalizationEnabled>
return {
"supported": (function(){
<#if realm.internationalizationEnabled>
var out= [];
<#list locale.supported as lng>
out.push({
"url": "${lng.url}",
"label": "${lng.label}",
"languageTag": "${lng.languageTag}"
});
</#list>
return out;
</#if>
return undefined;
})(),
"current": "${locale.current}"
};
</#if>
return undefined;
})(),
"auth": (function (){
<#if auth?has_content>
var out= {
"showUsername": ${auth.showUsername()?c},
"showResetCredentials": ${auth.showResetCredentials()?c},
"showTryAnotherWayLink": ${auth.showTryAnotherWayLink()?c},
"selectedCredential": "${auth.selectedCredential!''}" || undefined
};
<#if auth.showUsername() && !auth.showResetCredentials()>
Object.assign(
out,
{
"attemptedUsername": "${auth.attemptedUsername}"
}
);
</#if>
return out;
</#if>
return undefined;
})(),
"scripts": (function(){
var out = [];
<#if scripts??>
<#list scripts as script>
out.push("${script}");
</#list>
</#if>
return out;
})(),
"message": (function (){
<#if message?has_content>
return { 
"type": "${message.type}",
"summary": "${kcSanitize(message.summary)?no_esc}"
};
</#if>
return undefined;
})(),
"isAppInitiatedAction": (function (){
<#if isAppInitiatedAction??>
return true;
</#if>
return false;
})(),
"social": {
"displayInfo": ${social.displayInfo?c},
"providers": (()=>{
<#if social.providers??>
var out= [];
<#list social.providers as p>
out.push({
"loginUrl": "${p.loginUrl}",
"alias": "${p.alias}",
"providerId": "${p.providerId}",
"displayName": "${p.displayName}"
});
</#list>
return out;
</#if>
return undefined;
})()
},
"usernameEditDisabled": (function () {
<#if usernameEditDisabled??>
return true;
</#if>
return false;
})(),
"login": {
"username": "${login.username!''}" || undefined,
"rememberMe": (function (){
<#if login.rememberMe??>
return true;
</#if>
return false;
})()
},
"registrationDisabled": (function (){
<#if registrationDisabled??>
return true;
</#if>
return false;
}),
"messagesPerField": {
"printIfExists": function (key, x) {
switch(key){
case "userLabel": "${messagesPerField.printIfExists('userLabel','1'}" ? x : undefined;
case "username": "${messagesPerField.printIfExists('username','1'}" ? x : undefined;
case "email": "${messagesPerField.printIfExists('email','1'}" ? x : undefined;
case "firstName": "${messagesPerField.printIfExists('firstName','1'}" ? x : undefined;
case "lastName": "${messagesPerField.printIfExists('lastName','1'}" ? x : undefined;
case "password": "${messagesPerField.printIfExists('password','1'}" ? x : undefined;
case "password-confirm": "${messagesPerField.printIfExists('password-confirm','1'}" ? x : undefined;
}
}
},
"register": {
"formData": {
"firstName": "${register.formData.firstName!''}" || undefined,
"displayName": "${register.formData.displayName!''}" || undefined,
"lastName": "${register.formData.lastName!''}" || undefined,
"email": "${register.formData.email!''}" || undefined,
"username": "${register.formData.username!''}" || undefined
}
},
"passwordRequired": (function (){
<#if passwordRequired??>
return true;
</#if>
return false;
}),
"recaptchaRequired": (function (){
<#if passwordRequired??>
return true;
</#if>
return false;
}),
"recaptchaSiteKey": "${recaptchaSiteKey}"
}
</script>

View File

@ -7,7 +7,14 @@ import {
} from "../replaceImportFromStatic"; } from "../replaceImportFromStatic";
import fs from "fs"; import fs from "fs";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { objectKeys } from "evt/tools/typeSafety/objectKeys"; import { objectKeys } from "evt/tools/typeSafety/objectKeys";
function loadFtlFile(ftlFileBasename: "template.ftl" | "login.ftl" | "register.ftl") {
return fs.readFileSync(pathJoin(__dirname, ftlFileBasename))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1];
}
export function generateFtlFilesCodeFactory( export function generateFtlFilesCodeFactory(
params: { params: {
@ -51,11 +58,8 @@ 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 ftlPlaceholders = { const ftlCommonPlaceholders = {
'{ "x": "xIdLqMeOed9sdLdIdOxdK0d" }': '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadFtlFile("template.ftl"),
fs.readFileSync(pathJoin(__dirname, "ftl2js.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
'<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->': '<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->':
[ [
'<#if scripts??>', '<#if scripts??>',
@ -78,13 +82,13 @@ export function generateFtlFilesCodeFactory(
'' ''
]), ]),
'<script>', '<script>',
' Object.assign(', ' Object.deepAssign(',
` window.${ftlValuesGlobalName},`, ` window.${ftlValuesGlobalName},`,
` ${objectKeys(ftlPlaceholders)[0]}`, ` ${objectKeys(ftlCommonPlaceholders)[0]}`,
' );', ' );',
'</script>', '</script>',
'', '',
objectKeys(ftlPlaceholders)[1], objectKeys(ftlCommonPlaceholders)[1],
'' ''
].join("\n"), ].join("\n"),
); );
@ -102,11 +106,42 @@ export function generateFtlFilesCodeFactory(
const $ = cheerio.load(partiallyFixedIndexHtmlCode); const $ = cheerio.load(partiallyFixedIndexHtmlCode);
const ftlPlaceholders = {
'{ "x": "kxOlLqMeOed9sdLdIdOxd444" }': loadFtlFile(pageBasename),
...ftlCommonPlaceholders
};
$("head").prepend( $("head").prepend(
[ [
'', '',
'<script>', '<script>',
` window.${ftlValuesGlobalName} = { "pageBasename": "${pageBasename}" };`, '',
` window.${ftlValuesGlobalName} = Object.assign(`,
` { "pageBasename": "${pageBasename}" },`,
` ${objectKeys(ftlPlaceholders)[0]}`,
' );',
'',
' Object.defineProperty(',
' Object,',
' "deepAssign",',
' {',
' "value": function (target, source) {',
' Object.keys(source).forEach(function (key) {',
' var value = source[key];',
' if (value instanceof Object) {',
' if (!(target[key] instanceof Object)) {',
' target[key] = {};',
' }',
' deepAssign(target[key], value);',
' } else {',
' target[key] = value;',
' }',
' });',
' return target;',
' }',
' }',
' );',
'',
'</script>', '</script>',
'' ''
].join("\n") ].join("\n")

View File

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

View File

@ -0,0 +1,46 @@
<script>const _=
{
"url": {
"registrationAction": "${url.registrationAction}"
},
"messagesPerField": {
"printIfExists": function (key, x) {
switch(key){
case "userLabel": "${messagesPerField.printIfExists('userLabel','1'}" ? x : undefined;
case "username": "${messagesPerField.printIfExists('username','1'}" ? x : undefined;
case "email": "${messagesPerField.printIfExists('email','1'}" ? x : undefined;
case "firstName": "${messagesPerField.printIfExists('firstName','1'}" ? x : undefined;
case "lastName": "${messagesPerField.printIfExists('lastName','1'}" ? x : undefined;
case "password": "${messagesPerField.printIfExists('password','1'}" ? x : undefined;
case "password-confirm": "${messagesPerField.printIfExists('password-confirm','1'}" ? x : undefined;
}
}
},
"register": {
"formData": {
"firstName": "${register.formData.firstName!''}" || undefined,
"displayName": "${register.formData.displayName!''}" || undefined,
"lastName": "${register.formData.lastName!''}" || undefined,
"email": "${register.formData.email!''}" || undefined,
"username": "${register.formData.username!''}" || undefined
}
},
"passwordRequired": (function (){
<#if passwordRequired??>
return true;
</#if>
return false;
}),
"recaptchaRequired": (function (){
<#if passwordRequired??>
return true;
</#if>
return false;
}),
"recaptchaSiteKey": "${recaptchaSiteKey}"
}
</script>

View File

@ -0,0 +1,115 @@
<script>const _=
{
"url": {
"loginAction": "${url.loginAction}",
"resourcesPath": "${url.resourcesPath}",
"resourcesCommonPath": "${url.resourcesCommonPath}",
"loginRestartFlowUrl": "${url.loginRestartFlowUrl}",
"loginUrl": "${url.loginUrl}"
},
"realm": {
"displayName": "${realm.displayName!''}" || undefined,
"displayNameHtml": "${realm.displayNameHtml!''}" || undefined,
"internationalizationEnabled": ${realm.internationalizationEnabled?c},
"password": ${realm.password?c},
"registrationEmailAsUsername": ${realm.registrationEmailAsUsername?c},
},
"locale": (function (){
<#if realm.internationalizationEnabled>
return {
"supported": (function(){
<#if realm.internationalizationEnabled>
var out= [];
<#list locale.supported as lng>
out.push({
"url": "${lng.url}",
"label": "${lng.label}",
"languageTag": "${lng.languageTag}"
});
</#list>
return out;
</#if>
return undefined;
})(),
"current": "${locale.current}"
};
</#if>
return undefined;
})(),
"auth": (function (){
<#if auth?has_content>
var out= {
"showUsername": ${auth.showUsername()?c},
"showResetCredentials": ${auth.showResetCredentials()?c},
"showTryAnotherWayLink": ${auth.showTryAnotherWayLink()?c},
};
<#if auth.showUsername() && !auth.showResetCredentials()>
Object.assign(
out,
{
"attemptedUsername": "${auth.attemptedUsername}"
}
);
</#if>
return out;
</#if>
return undefined;
})(),
"scripts": (function(){
var out = [];
<#if scripts??>
<#list scripts as script>
out.push("${script}");
</#list>
</#if>
return out;
})(),
"message": (function (){
<#if message?has_content>
return { 
"type": "${message.type}",
"summary": "${kcSanitize(message.summary)?no_esc}"
};
</#if>
return undefined;
})(),
"isAppInitiatedAction": (function (){
<#if isAppInitiatedAction??>
return true;
</#if>
return false;
})()
}
</script>

View File

@ -1,7 +1,7 @@
import { memo } from "react"; import { memo } from "react";
import { kcContext } from "../kcContext"; import { kcContext } from "../kcContext";
import { assert } from "evt/tools/typeSafety/assert"; import { assert } from "../tools/assert";
import type { KcPagesProperties } from "./KcProperties"; import type { KcPagesProperties } from "./KcProperties";
import { Login } from "./Login"; import { Login } from "./Login";
import { Register } from "./Register"; import { Register } from "./Register";

View File

@ -3,8 +3,8 @@ import { useState, memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcPagesProperties } from "./KcProperties"; import type { KcPagesProperties } from "./KcProperties";
import { defaultKcPagesProperties } from "./KcProperties"; import { defaultKcPagesProperties } from "./KcProperties";
import { assert } from "evt/tools/typeSafety/assert"; import { assert } from "../tools/assert";
import { kcContext } from "../kcContext"; import { kcContext } from "../kcContext";
import { useKcTranslation } from "../i18n/useKcTranslation"; import { useKcTranslation } from "../i18n/useKcTranslation";
import { cx } from "tss-react"; import { cx } from "tss-react";
import { useConstCallback } from "powerhooks"; import { useConstCallback } from "powerhooks";
@ -25,13 +25,16 @@ export const Login = memo((props: LoginProps) => {
social, realm, url, social, realm, url,
usernameEditDisabled, login, usernameEditDisabled, login,
auth, registrationDisabled auth, registrationDisabled
}] = useState(() => ( }] = useState(() => {
assert( assert(
kcContext !== undefined, kcContext !== undefined &&
"App is currently being served by keycloak" kcContext.pageBasename === "login.ftl"
), );
kcContext
)); return kcContext;
});
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);

View File

@ -4,7 +4,7 @@ import { useState, memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcPagesProperties } from "./KcProperties"; import type { KcPagesProperties } from "./KcProperties";
import { defaultKcPagesProperties } from "./KcProperties"; import { defaultKcPagesProperties } from "./KcProperties";
import { assert } from "evt/tools/typeSafety/assert"; import { assert } from "../tools/assert";
import { kcContext } from "../kcContext"; import { kcContext } from "../kcContext";
import { useKcTranslation } from "../i18n/useKcTranslation"; import { useKcTranslation } from "../i18n/useKcTranslation";
import { cx } from "tss-react"; import { cx } from "tss-react";
@ -29,13 +29,16 @@ export const Register = memo((props: RegisterPageProps) => {
passwordRequired, passwordRequired,
recaptchaRequired, recaptchaRequired,
recaptchaSiteKey recaptchaSiteKey
}] = useState(() => ( }] = useState(() => {
assert( assert(
kcContext !== undefined, kcContext !== undefined &&
"App is currently being served by keycloak" kcContext.pageBasename === "register.ftl"
), );
kcContext
)); return kcContext;
});
return ( return (
<Template <Template

View File

@ -3,7 +3,7 @@ import { useState, useReducer ,useEffect, memo } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useKcTranslation } from "../i18n/useKcTranslation"; import { useKcTranslation } from "../i18n/useKcTranslation";
import { kcContext } from "../kcContext"; import { kcContext } from "../kcContext";
import { assert } from "evt/tools/typeSafety/assert"; import { assert } from "../tools/assert";
import { cx } from "tss-react"; import { cx } from "tss-react";
import { useKcLanguageTag } from "../i18n/useKcLanguageTag"; import { useKcLanguageTag } from "../i18n/useKcLanguageTag";
import type { KcLanguageTag } from "../i18n/KcLanguageTag"; import type { KcLanguageTag } from "../i18n/KcLanguageTag";

View File

@ -3,19 +3,18 @@ import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobal
import type { generateFtlFilesCodeFactory } from "../bin/build-keycloak-theme/generateFtl"; import type { generateFtlFilesCodeFactory } from "../bin/build-keycloak-theme/generateFtl";
import { id } from "evt/tools/typeSafety/id"; import { id } from "evt/tools/typeSafety/id";
import type { KcLanguageTag } from "./i18n/KcLanguageTag"; import type { KcLanguageTag } from "./i18n/KcLanguageTag";
import { doExtends } from "evt/tools/typeSafety/doExtends";
export type KcContext = KcContext.Login | KcContext.Register;
export type KcContext = { export declare namespace KcContext {
pageBasename: Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
export type Template = {
url: { url: {
loginAction: string; loginAction: string;
resourcesPath: string; resourcesPath: string;
resourcesCommonPath: string; resourcesCommonPath: string;
loginRestartFlowUrl: string; loginRestartFlowUrl: string;
loginResetCredentialsUrl: string;
registrationUrl: string;
//Specific to register
registrationAction: string;
loginUrl: string; loginUrl: string;
}; };
realm: { realm: {
@ -23,10 +22,7 @@ export type KcContext = {
displayNameHtml?: string; displayNameHtml?: string;
internationalizationEnabled: boolean; internationalizationEnabled: boolean;
password: boolean; password: boolean;
loginWithEmailAllowed: boolean;
registrationEmailAsUsername: boolean; registrationEmailAsUsername: boolean;
rememberMe: boolean;
resetPasswordAllowed: boolean;
}; };
/** Undefined if !realm.internationalizationEnabled */ /** Undefined if !realm.internationalizationEnabled */
locale?: { locale?: {
@ -47,7 +43,6 @@ export type KcContext = {
showResetCredentials: boolean; showResetCredentials: boolean;
showTryAnotherWayLink: boolean; showTryAnotherWayLink: boolean;
attemptedUsername?: boolean; attemptedUsername?: boolean;
selectedCredential?: string;
}; };
scripts: string[]; scripts: string[];
message?: { message?: {
@ -55,6 +50,28 @@ export type KcContext = {
summary: string; summary: string;
}; };
isAppInitiatedAction: boolean; isAppInitiatedAction: boolean;
};
export type Login = Template & {
pageBasename: "login.ftl";
url: {
loginResetCredentialsUrl: string;
registrationUrl: string;
};
realm: {
loginWithEmailAllowed: boolean;
rememberMe: boolean;
resetPasswordAllowed: boolean;
};
auth: {
selectedCredential?: string;
};
registrationDisabled: boolean;
login: {
username?: string;
rememberMe: boolean;
};
usernameEditDisabled: boolean;
social: { social: {
displayInfo: boolean; displayInfo: boolean;
providers?: { providers?: {
@ -64,13 +81,13 @@ export type KcContext = {
displayName: string; displayName: string;
}[] }[]
}; };
usernameEditDisabled: boolean;
login: {
username?: string;
rememberMe: boolean;
}; };
registrationDisabled: boolean;
//Specific to register export type Register = Template & {
pageBasename: "register.ftl";
url: {
registrationAction: string;
};
messagesPerField: { messagesPerField: {
printIfExists<T>( printIfExists<T>(
key: key:
@ -98,4 +115,14 @@ export type KcContext = {
recaptchaSiteKey: string; recaptchaSiteKey: string;
}; };
}
{
type T = KcContext["pageBasename"];
type U = Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
doExtends<T, U>();
doExtends<U, T>();
}
export const kcContext = id<KcContext | undefined>((window as any)[ftlValuesGlobalName]); export const kcContext = id<KcContext | undefined>((window as any)[ftlValuesGlobalName]);

View File

@ -1,125 +0,0 @@
import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobalName";
import type { generateFtlFilesCodeFactory } from "../bin/build-keycloak-theme/generateFtl";
import { id } from "evt/tools/typeSafety/id";
import type { KcLanguageTag } from "./i18n/KcLanguageTag";
import { doExtends } from "evt/tools/typeSafety/doExtends";
export type KcContext = KcContext.Login | KcContext.Register;
export declare namespace KcContext { 
export type Template = {};
export type Login = Template & { 
pageBasename: "login.ftl";
};
export type Register = Template & { 
pageBasename: "register.ftl";
};
}
{
type T = KcContext["pageBasename"];
type U = Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
doExtends<T, U>();
doExtends<U, T>();
}
export type KcContext = {
pageBasename: Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
url: {
loginAction: string;
resourcesPath: string;
resourcesCommonPath: string;
loginRestartFlowUrl: string;
loginResetCredentialsUrl: string;
registrationUrl: string;
registrationAction: string;
loginUrl: string;
};
realm: {
displayName?: string;
displayNameHtml?: string;
internationalizationEnabled: boolean;
password: boolean;
loginWithEmailAllowed: boolean;
registrationEmailAsUsername: boolean;
rememberMe: boolean;
resetPasswordAllowed: boolean;
};
/** Undefined if !realm.internationalizationEnabled */
locale?: {
supported: {
//url: string;
languageTag: KcLanguageTag;
/** Is determined by languageTag. Ex: languageTag === "en" => label === "English"
* or getLanguageLabel(languageTag) === label
*/
//label: LanguageLabel;
}[];
//NOTE: We do not expose this because the language is managed
//client side. We use this value however to set the default.
//current: LanguageLabel;
},
auth?: {
showUsername: boolean;
showResetCredentials: boolean;
showTryAnotherWayLink: boolean;
attemptedUsername?: boolean;
selectedCredential?: string;
};
scripts: string[];
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
isAppInitiatedAction: boolean;
social: {
displayInfo: boolean;
providers?: {
loginUrl: string;
alias: string;
providerId: string;
displayName: string;
}[]
};
usernameEditDisabled: boolean;
login: {
username?: string;
rememberMe: boolean;
};
registrationDisabled: boolean;
messagesPerField: {
printIfExists<T>(
key:
"userLabel" |
"username" |
"email" |
"firstName" |
"lastName" |
"password" |
"password-confirm",
x: T
): T | undefined;
};
register: {
formData: {
firstName?: string;
displayName?: string;
lastName?: string;
email?: string;
username?: string;
}
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey: string;
};
export const kcContext = id<KcContext | undefined>((window as any)[ftlValuesGlobalName]);

2
src/lib/tools/assert.ts Normal file
View File

@ -0,0 +1,2 @@
export { assert } from "evt/tools/typeSafety/assert";