Implement register

This commit is contained in:
Joseph Garrone 2021-03-04 18:15:48 +01:00
parent 09c7b6ac03
commit 2ee12abc43
9 changed files with 363 additions and 14 deletions

View File

@ -54,7 +54,7 @@
"dependencies": { "dependencies": {
"scripting-tools": "^0.19.13", "scripting-tools": "^0.19.13",
"cheerio": "^1.0.0-rc.5", "cheerio": "^1.0.0-rc.5",
"evt": "^1.9.12", "evt": "2.0.0-beta.15",
"minimal-polyfills": "^2.1.6", "minimal-polyfills": "^2.1.6",
"powerhooks": "^0.0.14", "powerhooks": "^0.0.14",
"tss-react": "^0.0.9" "tss-react": "^0.0.9"

View File

@ -6,7 +6,9 @@
"resourcesCommonPath": "${url.resourcesCommonPath}", "resourcesCommonPath": "${url.resourcesCommonPath}",
"loginRestartFlowUrl": "${url.loginRestartFlowUrl}", "loginRestartFlowUrl": "${url.loginRestartFlowUrl}",
"loginResetCredentialsUrl": "${url.loginResetCredentialsUrl}", "loginResetCredentialsUrl": "${url.loginResetCredentialsUrl}",
"registrationUrl": "${url.registrationUrl}" "registrationUrl": "${url.registrationUrl}",
"registrationAction": "${url.registrationUrl}",
"loginUrl": "${url.loginUrl}"
}, },
"realm": { "realm": {
"displayName": "${realm.displayName!''}" || undefined, "displayName": "${realm.displayName!''}" || undefined,
@ -168,6 +170,45 @@
</#if> </#if>
return false; 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> </script>

View File

@ -4,6 +4,7 @@ import { kcContext } from "../kcContext";
import { assert } from "evt/tools/typeSafety/assert"; import { assert } from "evt/tools/typeSafety/assert";
import type { KcPagesProperties } from "./KcProperties"; import type { KcPagesProperties } from "./KcProperties";
import { Login } from "./Login"; import { Login } from "./Login";
import { Register } from "./Register";
export type KcAppProps = { export type KcAppProps = {
kcProperties?: KcPagesProperties; kcProperties?: KcPagesProperties;
@ -16,10 +17,8 @@ export const KcApp = memo((props: KcAppProps) => {
assert(kcContext !== undefined, "App is not currently served by a Keycloak server"); assert(kcContext !== undefined, "App is not currently served by a Keycloak server");
switch (kcContext.pageBasename) { switch (kcContext.pageBasename) {
case "login.ftl": return <Login kcProperties={kcProperties} /> case "login.ftl": return <Login kcProperties={kcProperties} />;
case "register.ftl": case "register.ftl": return <Register kcProperties={kcProperties} />;
alert(`TODO: Implement ${kcContext.pageBasename}`);
return null;
} }
}); });

View File

@ -46,7 +46,6 @@ export const Login = memo((props: LoginProps) => {
displayWide={realm.password && social.providers !== undefined} displayWide={realm.password && social.providers !== undefined}
kcProperties={kcProperties} kcProperties={kcProperties}
headerNode={t("doLogIn")} headerNode={t("doLogIn")}
showUsernameNode={null}
formNode={ formNode={
<div <div
id="kc-form" id="kc-form"

View File

@ -0,0 +1,148 @@
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 { useKcTranslation } from "../i18n/useKcTranslation";
import { cx } from "tss-react";
export type RegisterPageProps = {
kcProperties?: KcPagesProperties;
};
export const Register = memo((props: RegisterPageProps) => {
const { kcProperties = {} } = props;
const { t, tStr } = useKcTranslation();
Object.assign(kcProperties, defaultKcPagesProperties);
const [{
url,
messagesPerField,
register,
realm,
passwordRequired,
recaptchaRequired,
recaptchaSiteKey
}] = useState(() => (
assert(
kcContext !== undefined,
"App is currently being served by keycloak"
),
kcContext
));
return (
<Template
kcProperties={kcProperties}
headerNode={t("registerTitle")}
formNode={
<form id="kc-register-form" className={cx(kcProperties.kcFormClass)} action={url.registrationAction} method="post">
<div className={cx(kcProperties.kcFormGroupClass, messagesPerField.printIfExists('firstName', kcProperties.kcFormGroupErrorClass))}>
<div className={cx(kcProperties.kcLabelWrapperClass)}>
<label htmlFor="firstName" className={cx(kcProperties.kcLabelClass)}>{t("firstName")}</label>
</div>
<div className={cx(kcProperties.kcInputWrapperClass)}>
<input type="text" id="firstName" className={cx(kcProperties.kcInputClass)} name="firstName"
value={register.formData.firstName ?? ""}
/>
</div>
</div>
<div className={cx(kcProperties.kcFormGroupClass, messagesPerField.printIfExists("lastName", kcProperties.kcFormGroupErrorClass))}>
<div className={cx(kcProperties.kcLabelWrapperClass)}>
<label htmlFor="lastName" className={cx(kcProperties.kcLabelClass)}>{t("lastName")}</label>
</div>
<div className={cx(kcProperties.kcInputWrapperClass)}>
<input type="text" id="lastName" className={cx(kcProperties.kcInputClass)} name="lastName"
value={register.formData.lastName ?? ""}
/>
</div>
</div>
<div className={cx(kcProperties.kcFormGroupClass, messagesPerField.printIfExists('email', kcProperties.kcFormGroupErrorClass))}>
<div className={cx(kcProperties.kcLabelWrapperClass)}>
<label htmlFor="email" className={cx(kcProperties.kcLabelClass)}>{t("email")}</label>
</div>
<div className={cx(kcProperties.kcInputWrapperClass)}>
<input type="text" id="email" className={cx(kcProperties.kcInputClass)} name="email"
value={register.formData.email ?? ""} autoComplete="email"
/>
</div>
</div>
{
!realm.registrationEmailAsUsername &&
<div className={cx(kcProperties.kcFormGroupClass, messagesPerField.printIfExists('username', kcProperties.kcFormGroupErrorClass))}>
<div className={cx(kcProperties.kcLabelWrapperClass)}>
<label htmlFor="username" className={cx(kcProperties.kcLabelClass)}>{t("username")}</label>
</div>
<div className={cx(kcProperties.kcInputWrapperClass)}>
<input type="text" id="username" className={cx(kcProperties.kcInputClass)} name="username"
value={register.formData.username ?? ""} autoComplete="username" />
</div>
</div >
}
{
passwordRequired &&
<>
<div className={cx(kcProperties.kcFormGroupClass, messagesPerField.printIfExists("password", kcProperties.kcFormGroupErrorClass))}>
<div className={cx(kcProperties.kcLabelWrapperClass)}>
<label htmlFor="password" className={cx(kcProperties.kcLabelClass)}>{t("password")}</label>
</div>
<div className={cx(kcProperties.kcInputWrapperClass)}>
<input type="password" id="password" className={cx(kcProperties.kcInputClass)} name="password" autoComplete="new-password" />
</div>
</div>
<div className={cx(kcProperties.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", kcProperties.kcFormGroupErrorClass))}>
<div className={cx(kcProperties.kcLabelWrapperClass)}>
<label htmlFor="password-confirm" className={cx(kcProperties.kcLabelClass)}>{t("passwordConfirm")}</label>
</div>
<div className={cx(kcProperties.kcInputWrapperClass)}>
<input type="password" id="password-confirm" className={cx(kcProperties.kcInputClass)} name="password-confirm" />
</div>
</div>
</>
}
{
recaptchaRequired &&
<div className="form-group">
<div className={cx(kcProperties.kcInputWrapperClass)}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
</div>
</div>
}
<div className={cx(kcProperties.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(kcProperties.kcFormOptionsClass)}>
<div className={cx(kcProperties.kcFormOptionsWrapperClass)}>
<span><a href={url.loginUrl}>{t("backToLogin")}</a></span>
</div>
</div>
<div id="kc-form-buttons" className={cx(kcProperties.kcFormButtonsClass)}>
<input className={cx(kcProperties.kcButtonClass, kcProperties.kcButtonPrimaryClass, kcProperties.kcButtonBlockClass, kcProperties.kcButtonLargeClass)} type="submit"
value={tStr("doRegister")} />
</div>
</div>
</form >
}
/>
);
});

View File

@ -23,9 +23,9 @@ export type TemplateProps = {
displayWide?: boolean; displayWide?: boolean;
showAnotherWayIfPresent?: boolean; showAnotherWayIfPresent?: boolean;
headerNode: ReactNode; headerNode: ReactNode;
showUsernameNode: ReactNode; showUsernameNode?: ReactNode;
formNode: ReactNode; formNode: ReactNode;
displayInfoNode: ReactNode; displayInfoNode?: ReactNode;
}; };
@ -39,9 +39,9 @@ export const Template = memo((props: TemplateProps) => {
showAnotherWayIfPresent = true, showAnotherWayIfPresent = true,
kcProperties = {}, kcProperties = {},
headerNode, headerNode,
showUsernameNode, showUsernameNode = null,
formNode, formNode,
displayInfoNode displayInfoNode = null
} = props; } = props;
const { t } = useKcTranslation(); const { t } = useKcTranslation();

View File

@ -2,7 +2,7 @@
import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobalName"; import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobalName";
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";
export type KcContext = { export type KcContext = {
@ -14,6 +14,9 @@ export type KcContext = {
loginRestartFlowUrl: string; loginRestartFlowUrl: string;
loginResetCredentialsUrl: string; loginResetCredentialsUrl: string;
registrationUrl: string; registrationUrl: string;
//Specific to register
registrationAction: string;
loginUrl: string;
}; };
realm: { realm: {
displayName?: string; displayName?: string;
@ -67,6 +70,32 @@ export type KcContext = {
rememberMe: boolean; rememberMe: boolean;
}; };
registrationDisabled: 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;
displayName?: string;
lastName?: string;
email?: string;
username?: string;
}
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey: string;
}; };
export const kcContext = id<KcContext | undefined>((window as any)[ftlValuesGlobalName]); export const kcContext = id<KcContext | undefined>((window as any)[ftlValuesGlobalName]);

View File

@ -0,0 +1,125 @@
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]);

View File

@ -623,7 +623,15 @@ evt@2.0.0-beta.12:
minimal-polyfills "^2.1.5" minimal-polyfills "^2.1.5"
run-exclusive "^2.2.14" run-exclusive "^2.2.14"
evt@^1.9.12, evt@^1.9.2: evt@2.0.0-beta.15:
version "2.0.0-beta.15"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.0.0-beta.15.tgz#a34ef224c827152c06f29157a9af5a41644e1a36"
integrity sha512-AZIuA8ujBsDX/rPC9mJ2sGvrBJAvV2OGj/yzACbuT4pRLKii5PMjYPitNk4xJMKuXSDwvwCsQzuGPDCcbRc60A==
dependencies:
minimal-polyfills "^2.1.5"
run-exclusive "^2.2.14"
evt@^1.9.2:
version "1.9.12" version "1.9.12"
resolved "https://registry.yarnpkg.com/evt/-/evt-1.9.12.tgz#8d06177259cbcb09ef936e18945bf7ddd087170c" resolved "https://registry.yarnpkg.com/evt/-/evt-1.9.12.tgz#8d06177259cbcb09ef936e18945bf7ddd087170c"
integrity sha512-u8wC4Xif2pcDJ9cEm0wzWCIQb+Y214m1eUgsgm2hVIuXuvC6LToryA0Ecl1O8Slii2E9l6USLsyxXWntjlnIbw== integrity sha512-u8wC4Xif2pcDJ9cEm0wzWCIQb+Y214m1eUgsgm2hVIuXuvC6LToryA0Ecl1O8Slii2E9l6USLsyxXWntjlnIbw==