Implement LoginVerifyEmail

This commit is contained in:
Joseph Garrone 2021-03-07 15:37:37 +01:00
parent 438ca4595f
commit adc6d69201
12 changed files with 126 additions and 67 deletions

View File

@ -9,7 +9,7 @@ import fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
export const pageIds= [ "login.ftl", "register.ftl", "info.ftl", "error.ftl", "login-reset-password.ftl"] as const;
export const pageIds = ["login.ftl", "register.ftl", "info.ftl", "error.ftl", "login-reset-password.ftl", "login-verify-email.ftl"] as const;
export type PageId = typeof pageIds[number];
@ -19,8 +19,14 @@ function loadAdjacentFile(fileBasename: string){
};
function loadFtlFile(ftlFileBasename: PageId | "template.ftl") {
return loadAdjacentFile(ftlFileBasename)
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1];
try {
return loadAdjacentFile(ftlFileBasename)
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1];
} catch {
return "{}";
}
}
export function generateFtlFilesCodeFactory(

View File

@ -4,11 +4,11 @@ import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import { kcContext } from "../kcContext";
import { useKcTranslation } from "../i18n/useKcTranslation";
import { useKcMessage } from "../i18n/useKcMessage";
export const Error = memo((props: KcProps) => {
const { t } = useKcTranslation();
const { msg } = useKcMessage();
assert(
kcContext !== undefined &&
@ -22,7 +22,7 @@ export const Error = memo((props: KcProps) => {
<Template
{...props}
displayMessage={false}
headerNode={t("errorTitle")}
headerNode={msg("errorTitle")}
formNode={
<div id="kc-error-message">
<p className="instruction">{message.summary}</p>
@ -30,7 +30,7 @@ export const Error = memo((props: KcProps) => {
client !== undefined && client.baseUrl !== undefined &&
<p>
<a id="backToApplication" href={client.baseUrl}>
{t("backToApplication")}
{msg("backToApplication")}
</a>
</p>
}

View File

@ -4,11 +4,11 @@ import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import { kcContext } from "../kcContext";
import { useKcTranslation } from "../i18n/useKcTranslation";
import { useKcMessage } from "../i18n/useKcMessage";
export const Info = memo((props: KcProps) => {
const { t } = useKcTranslation();
const { msg } = useKcMessage();
assert(
kcContext !== undefined &&
@ -45,7 +45,7 @@ export const Info = memo((props: KcProps) => {
<b>
{
requiredActions
.map(requiredAction => t(`requiredAction.${requiredAction}` as const))
.map(requiredAction => msg(`requiredAction.${requiredAction}` as const))
.join(",")
}
@ -57,13 +57,13 @@ export const Info = memo((props: KcProps) => {
{
!skipLink &&
pageRedirectUri !== undefined ?
<p><a href="${pageRedirectUri}">${(t("backToApplication"))}</a></p>
<p><a href="${pageRedirectUri}">${(msg("backToApplication"))}</a></p>
:
actionUri !== undefined ?
<p><a href="${actionUri}">${t("proceedWithAction")}</a></p>
<p><a href="${actionUri}">${msg("proceedWithAction")}</a></p>
:
client.baseUrl !== undefined &&
<p><a href="${client.baseUrl}">${t("backToApplication")}</a></p>
<p><a href="${client.baseUrl}">${msg("backToApplication")}</a></p>
}
</div>

View File

@ -8,7 +8,7 @@ import { Register } from "./Register";
import { Info } from "./Info";
import { Error } from "./Error";
import { LoginResetPassword } from "./LoginResetPassword";
import { LoginVerifyEmail } from "./LoginVerifyEmail";
export const KcApp = memo((props: KcProps) => {
@ -20,6 +20,7 @@ export const KcApp = memo((props: KcProps) => {
case "info.ftl": return <Info {...props} />;
case "error.ftl": return <Error {...props} />;
case "login-reset-password.ftl": return <LoginResetPassword {...props} />;
case "login-verify-email.ftl": return <LoginVerifyEmail {...props} />;
}
});

View File

@ -4,13 +4,13 @@ import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import { kcContext } from "../kcContext";
import { useKcTranslation } from "../i18n/useKcTranslation";
import { useKcMessage } from "../i18n/useKcMessage";
import { cx } from "tss-react";
import { useConstCallback } from "powerhooks";
export const Login = memo((props: KcProps) => {
const { t, tStr } = useKcTranslation();
const { msg, msgStr } = useKcMessage();
assert(
kcContext !== undefined &&
@ -34,7 +34,7 @@ export const Login = memo((props: KcProps) => {
{...props}
displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined}
headerNode={t("doLogIn")}
headerNode={msg("doLogIn")}
formNode={
<div
id="kc-form"
@ -52,12 +52,12 @@ export const Login = memo((props: KcProps) => {
<label htmlFor="username" className={cx(props.kcLabelClass)}>
{
!realm.loginWithEmailAllowed ?
t("username")
msg("username")
:
(
!realm.registrationEmailAsUsername ?
t("usernameOrEmail") :
t("email")
msg("usernameOrEmail") :
msg("email")
)
}
</label>
@ -73,7 +73,7 @@ export const Login = memo((props: KcProps) => {
</div>
<div className={cx(props.kcFormGroupClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}>
{t("password")}
{msg("password")}
</label>
<input tabIndex={2} id="password" className={cx(props.kcInputClass)} name="password" type="password" autoComplete="off" />
</div>
@ -87,7 +87,7 @@ export const Login = memo((props: KcProps) => {
<div className="checkbox">
<label>
<input tabIndex={3} id="rememberMe" name="rememberMe" type="checkbox" {...(login.rememberMe ? { "checked": true } : {})} />
{t("rememberMe")}
{msg("rememberMe")}
</label>
</div>
}
@ -96,7 +96,7 @@ export const Login = memo((props: KcProps) => {
{
realm.resetPasswordAllowed &&
<span>
<a tabIndex={5} href={url.loginResetCredentialsUrl}>{t("doForgotPassword")}</a>
<a tabIndex={5} href={url.loginResetCredentialsUrl}>{msg("doForgotPassword")}</a>
</span>
}
</div>
@ -112,7 +112,7 @@ export const Login = memo((props: KcProps) => {
<input
tabIndex={4}
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} name="login" id="kc-login" type="submit"
value={tStr("doLogIn")}
value={msgStr("doLogIn")}
disabled={isLoginButtonDisabled}
/>
</div>
@ -138,7 +138,7 @@ export const Login = memo((props: KcProps) => {
}
</div>
}
displayInfoNode={
infoNode={
(
realm.password &&
realm.registrationAllowed &&
@ -146,9 +146,9 @@ export const Login = memo((props: KcProps) => {
) &&
<div id="kc-registration">
<span>
{t("noAccount")}
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{t("doRegister")}
{msg("doRegister")}
</a>
</span>
</div>

View File

@ -4,12 +4,12 @@ import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import { kcContext } from "../kcContext";
import { useKcTranslation } from "../i18n/useKcTranslation";
import { useKcMessage } from "../i18n/useKcMessage";
import { cx } from "tss-react";
export const LoginResetPassword = memo((props: KcProps) => {
const { t, tStr } = useKcTranslation();
const { msg, msgStr } = useKcMessage();
assert(
kcContext !== undefined &&
@ -26,7 +26,7 @@ export const LoginResetPassword = memo((props: KcProps) => {
<Template
{...props}
displayMessage={false}
headerNode={t("emailForgotTitle")}
headerNode={msg("emailForgotTitle")}
formNode={
<form id="kc-reset-password-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
<div className={cx(props.kcFormGroupClass)}>
@ -34,11 +34,11 @@ export const LoginResetPassword = memo((props: KcProps) => {
<label htmlFor="username" className={cx(props.kcLabelClass)}>
{
!realm.loginWithEmailAllowed ?
t("username")
msg("username")
:
!realm.registrationEmailAsUsername ?
t("usernameOrEmail") :
t("email")
msg("usernameOrEmail") :
msg("email")
}
</label>
</div>
@ -60,7 +60,7 @@ export const LoginResetPassword = memo((props: KcProps) => {
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span>
<a href={url.loginUrl}>{t("backToLogin")}</a>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</div>
@ -72,13 +72,13 @@ export const LoginResetPassword = memo((props: KcProps) => {
props.kcButtonBlockClass, props.kcButtonLargeClass
)}
type="submit"
defaultValue={tStr("doSubmit")}
defaultValue={msgStr("doSubmit")}
/>
</div>
</div>
</form>
}
infoNode={t("emailInstruction")}
infoNode={msg("emailInstruction")}
/>
);
});

View File

@ -0,0 +1,44 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import { kcContext } from "../kcContext";
import { useKcMessage } from "../i18n/useKcMessage";
export const LoginVerifyEmail = memo((props: KcProps) => {
const { msg } = useKcMessage();
assert(
kcContext !== undefined &&
kcContext.pageId === "login-verify-email.ftl"
);
const {
url
} = kcContext;
return (
<Template
{...props}
displayMessage={false}
headerNode={msg("emailVerifyTitle")}
formNode={
<>
<p className="instruction">
{msg("emailVerifyInstruction1")}
</p>
<p className="instruction">
{msg("emailVerifyInstruction2")}
<a href={url.loginAction}>{msg("doClickHere")}</a>
{msg("emailVerifyInstruction3")}
</p>
</>
}
/>
);
});

View File

@ -3,12 +3,12 @@ import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import { kcContext } from "../kcContext";
import { useKcTranslation } from "../i18n/useKcTranslation";
import { useKcMessage } from "../i18n/useKcMessage";
import { cx } from "tss-react";
export const Register = memo((props: KcProps) => {
const { t, tStr } = useKcTranslation();
const { msg, msgStr } = useKcMessage();
assert(
kcContext !== undefined &&
@ -28,13 +28,13 @@ export const Register = memo((props: KcProps) => {
return (
<Template
{...props}
headerNode={t("registerTitle")}
headerNode={msg("registerTitle")}
formNode={
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists('firstName', props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>{t("firstName")}</label>
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>{msg("firstName")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="firstName" className={cx(props.kcInputClass)} name="firstName"
@ -45,7 +45,7 @@ export const Register = memo((props: KcProps) => {
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="lastName" className={cx(props.kcLabelClass)}>{t("lastName")}</label>
<label htmlFor="lastName" className={cx(props.kcLabelClass)}>{msg("lastName")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="lastName" className={cx(props.kcInputClass)} name="lastName"
@ -56,7 +56,7 @@ export const Register = memo((props: KcProps) => {
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists('email', props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="email" className={cx(props.kcLabelClass)}>{t("email")}</label>
<label htmlFor="email" className={cx(props.kcLabelClass)}>{msg("email")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="email" className={cx(props.kcInputClass)} name="email"
@ -69,7 +69,7 @@ export const Register = memo((props: KcProps) => {
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists('username', props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}>{t("username")}</label>
<label htmlFor="username" className={cx(props.kcLabelClass)}>{msg("username")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="username" className={cx(props.kcInputClass)} name="username"
@ -84,7 +84,7 @@ export const Register = memo((props: KcProps) => {
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}>{t("password")}</label>
<label htmlFor="password" className={cx(props.kcLabelClass)}>{msg("password")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="password" id="password" className={cx(props.kcInputClass)} name="password" autoComplete="new-password" />
@ -93,7 +93,7 @@ export const Register = memo((props: KcProps) => {
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>{t("passwordConfirm")}</label>
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>{msg("passwordConfirm")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="password" id="password-confirm" className={cx(props.kcInputClass)} name="password-confirm" />
@ -113,13 +113,13 @@ export const Register = memo((props: KcProps) => {
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span><a href={url.loginUrl}>{t("backToLogin")}</a></span>
<span><a href={url.loginUrl}>{msg("backToLogin")}</a></span>
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} type="submit"
defaultValue={tStr("doRegister")} />
defaultValue={msgStr("doRegister")} />
</div>
</div>
</form >

View File

@ -1,7 +1,7 @@
import { useReducer, useEffect, memo } from "react";
import type { ReactNode } from "react";
import { useKcTranslation } from "../i18n/useKcTranslation";
import { useKcMessage } from "../i18n/useKcMessage";
import { useKcLanguageTag } from "../i18n/useKcLanguageTag";
import { kcContext } from "../kcContext";
import { assert } from "../tools/assert";
@ -44,7 +44,7 @@ export const Template = memo((props: TemplateProps) => {
useEffect(() => { console.log("Rendering this page with react using keycloakify") }, []);
const { t } = useKcTranslation();
const { msg } = useKcMessage();
const { kcLanguageTag, setKcLanguageTag } = useKcLanguageTag();
@ -132,7 +132,7 @@ export const Template = memo((props: TemplateProps) => {
<div id="kc-header" className={cx(props.kcHeaderClass)}>
<div id="kc-header-wrapper" className={cx(props.kcHeaderWrapperClass)}>
{t("loginTitleHtml", realm.displayNameHtml)}
{msg("loginTitleHtml", realm.displayNameHtml)}
</div>
</div>
@ -182,7 +182,7 @@ export const Template = memo((props: TemplateProps) => {
<div className={cx(props.kcLabelWrapperClass, "subtitle")}>
<span className="subtitle">
<span className="required">*</span>
{t("requiredFields")}
{msg("requiredFields")}
</span>
</div>
<div className="col-md-10">
@ -201,7 +201,7 @@ export const Template = memo((props: TemplateProps) => {
displayRequiredFields ? (
<div className={cx(props.kcContentWrapperClass)}>
<div className={cx(props.kcLabelWrapperClass, "subtitle")}>
<span className="subtitle"><span className="required">*</span> {t("requiredFields")}</span>
<span className="subtitle"><span className="required">*</span> {msg("requiredFields")}</span>
</div>
<div className="col-md-10">
{showUsernameNode}
@ -211,7 +211,7 @@ export const Template = memo((props: TemplateProps) => {
<a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip">
<i className={cx(props.kcResetFlowIcon)}></i>
<span className="kc-tooltip-text">{t("restartLoginTooltip")}</span>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
@ -227,7 +227,7 @@ export const Template = memo((props: TemplateProps) => {
<a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip">
<i className={cx(props.kcResetFlowIcon)}></i>
<span className="kc-tooltip-text">{t("restartLoginTooltip")}</span>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
@ -269,7 +269,7 @@ export const Template = memo((props: TemplateProps) => {
<div className={cx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])} >
<div className={cx(props.kcFormGroupClass)}>
<input type="hidden" name="tryAnotherWay" value="on" />
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>{t("doTryAnotherWay")}</a>
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>{msg("doTryAnotherWay")}</a>
</div>
</div >
</form>

View File

@ -7,11 +7,11 @@ import { id } from "evt/tools/typeSafety/id";
export type MessageKey = keyof typeof messages["en"];
export function useKcTranslation() {
export function useKcMessage() {
const { kcLanguageTag } = useKcLanguageTag();
const tStr = useConstCallback(
const msgStr = useConstCallback(
(key: MessageKey, ...args: (string | undefined)[]): string => {
let str: string = messages[kcLanguageTag as any as "en"][key] ?? messages["en"][key];
@ -31,13 +31,13 @@ export function useKcTranslation() {
}
);
const t = useConstCallback(
id<(...args: Parameters<typeof tStr>) => ReactNode>(
const msg = useConstCallback(
id<(...args: Parameters<typeof msgStr>) => ReactNode>(
(key, ...args) =>
<span className={key} dangerouslySetInnerHTML={{ "__html": tStr(key, ...args) }} />
<span className={key} dangerouslySetInnerHTML={{ "__html": msgStr(key, ...args) }} />
)
);
return { t, tStr };
return { msg, msgStr };
}

View File

@ -2,7 +2,7 @@ export * from "./kcContext";
export * from "./i18n/KcLanguageTag";
export * from "./i18n/useKcLanguageTag";
export * from "./i18n/useKcTranslation";
export * from "./i18n/useKcMessage";
export * from "./components/KcProps";
export * from "./components/Login";
@ -10,5 +10,7 @@ export * from "./components/Template";
export * from "./components/KcApp";
export * from "./components/Info";
export * from "./components/Error";
export * from "./components/LoginResetPassword";
export * from "./components/LoginVerifyEmail";
export * from "./tools/assert";

View File

@ -4,13 +4,15 @@ import type { PageId } 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";
import type { MessageKey } from "./i18n/useKcTranslation";
import type { LanguageLabel } from "./i18n/KcLanguageTag";
import type { MessageKey } from "./i18n/useKcMessage";
import type { LanguageLabel } from "./i18n/KcLanguageTag";
type ExtractAfterStartingWith<Prefix extends string, StrEnum> =
type ExtractAfterStartingWith<Prefix extends string, StrEnum> =
StrEnum extends `${Prefix}${infer U}` ? U : never;
export type KcContext = KcContext.Login | KcContext.Register | KcContext.Info | KcContext.Error | KcContext.LoginResetPassword;
export type KcContext =
KcContext.Login | KcContext.Register | KcContext.Info |
KcContext.Error | KcContext.LoginResetPassword | KcContext.LoginVerifyEmail;
export declare namespace KcContext {
export type Template = {
@ -122,7 +124,7 @@ export declare namespace KcContext {
export type Info = Template & {
pageId: "info.ftl";
messageHeader?: string;
requiredActions?: ExtractAfterStartingWith<"requiredAction.",MessageKey>[];
requiredActions?: ExtractAfterStartingWith<"requiredAction.", MessageKey>[];
skipLink: boolean;
pageRedirectUri?: string;
actionUri?: string;
@ -145,6 +147,10 @@ export declare namespace KcContext {
}
};
export type LoginVerifyEmail = Template & {
pageId: "login-verify-email.ftl";
};
}
doExtends<KcContext["pageId"], PageId>();