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";
import fs from "fs";
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(
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.
const ftlPlaceholders = {
'{ "x": "xIdLqMeOed9sdLdIdOxdK0d" }':
fs.readFileSync(pathJoin(__dirname, "ftl2js.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
const ftlCommonPlaceholders = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadFtlFile("template.ftl"),
'<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->':
[
'<#if scripts??>',
@ -78,13 +82,13 @@ export function generateFtlFilesCodeFactory(
''
]),
'<script>',
' Object.assign(',
' Object.deepAssign(',
` window.${ftlValuesGlobalName},`,
` ${objectKeys(ftlPlaceholders)[0]}`,
` ${objectKeys(ftlCommonPlaceholders)[0]}`,
' );',
'</script>',
'',
objectKeys(ftlPlaceholders)[1],
objectKeys(ftlCommonPlaceholders)[1],
''
].join("\n"),
);
@ -102,11 +106,42 @@ export function generateFtlFilesCodeFactory(
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
const ftlPlaceholders = {
'{ "x": "kxOlLqMeOed9sdLdIdOxd444" }': loadFtlFile(pageBasename),
...ftlCommonPlaceholders
};
$("head").prepend(
[
'',
'<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>',
''
].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 { kcContext } from "../kcContext";
import { assert } from "evt/tools/typeSafety/assert";
import { assert } from "../tools/assert";
import type { KcPagesProperties } from "./KcProperties";
import { Login } from "./Login";
import { Register } from "./Register";

View File

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

View File

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

View File

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

View File

@ -3,99 +3,126 @@ import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobal
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 type KcContext = {
pageBasename: Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
url: {
loginAction: string;
resourcesPath: string;
resourcesCommonPath: string;
loginRestartFlowUrl: string;
loginResetCredentialsUrl: string;
registrationUrl: string;
//Specific to register
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?: {
export declare namespace KcContext {
export type Template = {
url: {
loginAction: string;
resourcesPath: string;
resourcesCommonPath: string;
loginRestartFlowUrl: string;
loginUrl: string;
alias: string;
providerId: string;
displayName: string;
}[]
};
usernameEditDisabled: boolean;
login: {
username?: string;
rememberMe: boolean;
};
registrationDisabled: boolean;
//Specific to register
messagesPerField: {
printIfExists<T>(
key:
"userLabel" |
"username" |
"email" |
"firstName" |
"lastName" |
"password" |
"password-confirm",
x: T
): T | undefined;
};
register: {
formData: {
firstName?: string;
};
realm: {
displayName?: string;
lastName?: string;
email?: string;
username?: string;
}
displayNameHtml?: string;
internationalizationEnabled: boolean;
password: boolean;
registrationEmailAsUsername: 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;
};
scripts: string[];
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
isAppInitiatedAction: boolean;
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey: string;
};
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: {
displayInfo: boolean;
providers?: {
loginUrl: string;
alias: string;
providerId: string;
displayName: string;
}[]
};
};
export type Register = Template & {
pageBasename: "register.ftl";
url: {
registrationAction: string;
};
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;
};
}
{
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]);

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";