Add login-recovery-authn-code-config.ftl page
This commit is contained in:
parent
834ae4e45b
commit
060b1ea8b8
@ -29,7 +29,8 @@ export const loginThemePageIds = [
|
|||||||
"delete-credential.ftl",
|
"delete-credential.ftl",
|
||||||
"code.ftl",
|
"code.ftl",
|
||||||
"delete-account-confirm.ftl",
|
"delete-account-confirm.ftl",
|
||||||
"frontchannel-logout.ftl"
|
"frontchannel-logout.ftl",
|
||||||
|
"login-recovery-authn-code-config.ftl"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl", "totp.ftl", "applications.ftl", "log.ftl"] as const;
|
export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl", "totp.ftl", "applications.ftl", "log.ftl"] as const;
|
||||||
|
@ -35,6 +35,7 @@ const DeleteCredential = lazy(() => import("keycloakify/login/pages/DeleteCreden
|
|||||||
const Code = lazy(() => import("keycloakify/login/pages/Code"));
|
const Code = lazy(() => import("keycloakify/login/pages/Code"));
|
||||||
const DeleteAccountConfirm = lazy(() => import("keycloakify/login/pages/DeleteAccountConfirm"));
|
const DeleteAccountConfirm = lazy(() => import("keycloakify/login/pages/DeleteAccountConfirm"));
|
||||||
const FrontchannelLogout = lazy(() => import("keycloakify/login/pages/FrontchannelLogout"));
|
const FrontchannelLogout = lazy(() => import("keycloakify/login/pages/FrontchannelLogout"));
|
||||||
|
const LoginRecoveryAuthnCodeConfig = lazy(() => import("keycloakify/login/pages/LoginRecoveryAuthnCodeConfig"));
|
||||||
|
|
||||||
type FallbackProps = PageProps<KcContext, I18n> & {
|
type FallbackProps = PageProps<KcContext, I18n> & {
|
||||||
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
||||||
@ -107,6 +108,8 @@ export default function Fallback(props: FallbackProps) {
|
|||||||
return <DeleteAccountConfirm kcContext={kcContext} {...rest} />;
|
return <DeleteAccountConfirm kcContext={kcContext} {...rest} />;
|
||||||
case "frontchannel-logout.ftl":
|
case "frontchannel-logout.ftl":
|
||||||
return <FrontchannelLogout kcContext={kcContext} {...rest} />;
|
return <FrontchannelLogout kcContext={kcContext} {...rest} />;
|
||||||
|
case "login-recovery-authn-code-config.ftl":
|
||||||
|
return <LoginRecoveryAuthnCodeConfig kcContext={kcContext} {...rest} />;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof kcContext, never>>(false);
|
assert<Equals<typeof kcContext, never>>(false);
|
||||||
})()}
|
})()}
|
||||||
|
@ -39,7 +39,8 @@ export type KcContext =
|
|||||||
| KcContext.DeleteCredential
|
| KcContext.DeleteCredential
|
||||||
| KcContext.Code
|
| KcContext.Code
|
||||||
| KcContext.DeleteAccountConfirm
|
| KcContext.DeleteAccountConfirm
|
||||||
| KcContext.FrontchannelLogout;
|
| KcContext.FrontchannelLogout
|
||||||
|
| KcContext.LoginRecoveryAuthnCodeConfig;
|
||||||
|
|
||||||
assert<KcContext["themeType"] extends ThemeType ? true : false>();
|
assert<KcContext["themeType"] extends ThemeType ? true : false>();
|
||||||
|
|
||||||
@ -528,6 +529,15 @@ export declare namespace KcContext {
|
|||||||
logoutRedirectUri?: string;
|
logoutRedirectUri?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LoginRecoveryAuthnCodeConfig = Common & {
|
||||||
|
pageId: "login-recovery-authn-code-config.ftl";
|
||||||
|
recoveryAuthnCodesConfigBean: {
|
||||||
|
generatedRecoveryAuthnCodesList: string[];
|
||||||
|
generatedRecoveryAuthnCodesAsString: string;
|
||||||
|
generatedAt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserProfile = {
|
export type UserProfile = {
|
||||||
|
255
src/login/pages/LoginRecoveryAuthnCodeConfig.tsx
Normal file
255
src/login/pages/LoginRecoveryAuthnCodeConfig.tsx
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
|
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||||
|
import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
const { useInsertScriptTags } = createUseInsertScriptTags();
|
||||||
|
|
||||||
|
export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<KcContext, { pageId: "login-recovery-authn-code-config.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const { getClassName } = useGetClassName({
|
||||||
|
doUseDefaultCss,
|
||||||
|
classes
|
||||||
|
});
|
||||||
|
|
||||||
|
const { recoveryAuthnCodesConfigBean, isAppInitiatedAction } = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
useInsertScriptTags({
|
||||||
|
"scriptTags": [
|
||||||
|
{
|
||||||
|
"type": "text/javascript",
|
||||||
|
"textContent": `
|
||||||
|
|
||||||
|
/* copy recovery codes */
|
||||||
|
function copyRecoveryCodes() {
|
||||||
|
var tmpTextarea = document.createElement("textarea");
|
||||||
|
var codes = document.getElementById("kc-recovery-codes-list").getElementsByTagName("li");
|
||||||
|
for (i = 0; i < codes.length; i++) {
|
||||||
|
tmpTextarea.value = tmpTextarea.value + codes[i].innerText + "\n";
|
||||||
|
}
|
||||||
|
document.body.appendChild(tmpTextarea);
|
||||||
|
tmpTextarea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(tmpTextarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
var copyButton = document.getElementById("copyRecoveryCodes");
|
||||||
|
copyButton && copyButton.addEventListener("click", function () {
|
||||||
|
copyRecoveryCodes();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* download recovery codes */
|
||||||
|
function formatCurrentDateTime() {
|
||||||
|
var dt = new Date();
|
||||||
|
var options = {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZoneName: 'short'
|
||||||
|
};
|
||||||
|
|
||||||
|
return dt.toLocaleString('en-US', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRecoveryCodeList() {
|
||||||
|
var recoveryCodes = document.querySelectorAll(".kc-recovery-codes-list li");
|
||||||
|
var recoveryCodeList = "";
|
||||||
|
|
||||||
|
for (var i = 0; i < recoveryCodes.length; i++) {
|
||||||
|
var recoveryCodeLiElement = recoveryCodes[i].innerText;
|
||||||
|
recoveryCodeList += recoveryCodeLiElement + "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return recoveryCodeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDownloadContent() {
|
||||||
|
var recoveryCodeList = parseRecoveryCodeList();
|
||||||
|
var dt = new Date();
|
||||||
|
var options = {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZoneName: 'short'
|
||||||
|
};
|
||||||
|
|
||||||
|
return fileBodyContent =
|
||||||
|
"${msgStr("recovery-codes-download-file-header")}\n\n" +
|
||||||
|
recoveryCodeList + "\n" +
|
||||||
|
"${msgStr("recovery-codes-download-file-description")}\n\n" +
|
||||||
|
"${msgStr("recovery-codes-download-file-date")} " + formatCurrentDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpDownloadLinkAndDownload(filename, text) {
|
||||||
|
var el = document.createElement('a');
|
||||||
|
el.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||||
|
el.setAttribute('download', filename);
|
||||||
|
el.style.display = 'none';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.click();
|
||||||
|
document.body.removeChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadRecoveryCodes() {
|
||||||
|
setUpDownloadLinkAndDownload('kc-download-recovery-codes.txt', buildDownloadContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadButton = document.getElementById("downloadRecoveryCodes");
|
||||||
|
downloadButton && downloadButton.addEventListener("click", downloadRecoveryCodes);
|
||||||
|
|
||||||
|
/* print recovery codes */
|
||||||
|
function buildPrintContent() {
|
||||||
|
var recoveryCodeListHTML = document.getElementById('kc-recovery-codes-list').innerHTML;
|
||||||
|
var styles =
|
||||||
|
\`@page { size: auto; margin-top: 0; }
|
||||||
|
body { width: 480px; }
|
||||||
|
div { list-style-type: none; font-family: monospace }
|
||||||
|
p:first-of-type { margin-top: 48px }\`;
|
||||||
|
|
||||||
|
return printFileContent =
|
||||||
|
"<html><style>" + styles + "</style><body>" +
|
||||||
|
"<title>kc-download-recovery-codes</title>" +
|
||||||
|
"<p>${msgStr("recovery-codes-download-file-header")}</p>" +
|
||||||
|
"<div>" + recoveryCodeListHTML + "</div>" +
|
||||||
|
"<p>${msgStr("recovery-codes-download-file-description")}</p>" +
|
||||||
|
"<p>${msgStr("recovery-codes-download-file-date")} " + formatCurrentDateTime() + "</p>" +
|
||||||
|
"</body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function printRecoveryCodes() {
|
||||||
|
var w = window.open();
|
||||||
|
w.document.write(buildPrintContent());
|
||||||
|
w.print();
|
||||||
|
w.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
var printButton = document.getElementById("printRecoveryCodes");
|
||||||
|
printButton && printButton.addEventListener("click", printRecoveryCodes);
|
||||||
|
`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("recovery-code-config-header")}>
|
||||||
|
<div className={clsx("pf-c-alert", "pf-m-warning", "pf-m-inline", getClassName("kcRecoveryCodesWarning"))} aria-label="Warning alert">
|
||||||
|
<div className="pf-c-alert__icon">
|
||||||
|
<i className="pficon-warning-triangle-o" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<h4 className="pf-c-alert__title">
|
||||||
|
<span className="pf-screen-reader">Warning alert:</span>
|
||||||
|
{msg("recovery-code-config-warning-title")}
|
||||||
|
</h4>
|
||||||
|
<div className="pf-c-alert__description">
|
||||||
|
<p>{msg("recovery-code-config-warning-message")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ol id="kc-recovery-codes-list" className={getClassName("kcRecoveryCodesList")}>
|
||||||
|
{recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesList.map((code, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<span>{index + 1}:</span> {code.slice(0, 4)}-{code.slice(4, 8)}-{code.slice(8)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
{/* actions */}
|
||||||
|
<div className={getClassName("kcRecoveryCodesActions")}>
|
||||||
|
<button id="printRecoveryCodes" className={clsx("pf-c-button", "pf-m-link")} type="button">
|
||||||
|
<i className="pficon-print" aria-hidden="true" /> {msg("recovery-codes-print")}
|
||||||
|
</button>
|
||||||
|
<button id="downloadRecoveryCodes" className={clsx("pf-c-button", "pf-m-link")} type="button">
|
||||||
|
<i className="pficon-save" aria-hidden="true" /> {msg("recovery-codes-download")}
|
||||||
|
</button>
|
||||||
|
<button id="copyRecoveryCodes" className={clsx("pf-c-button", "pf-m-link")} type="button">
|
||||||
|
<i className="pficon-blueprint" aria-hidden="true" /> {msg("recovery-codes-copy")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* confirmation checkbox */}
|
||||||
|
<div className={getClassName("kcFormOptionsClass")}>
|
||||||
|
<input
|
||||||
|
className={getClassName("kcCheckInputClass")}
|
||||||
|
type="checkbox"
|
||||||
|
id="kcRecoveryCodesConfirmationCheck"
|
||||||
|
name="kcRecoveryCodesConfirmationCheck"
|
||||||
|
onChange={function () {
|
||||||
|
//@ts-expect-error: This is code from the original theme, we trust it.
|
||||||
|
document.getElementById("saveRecoveryAuthnCodesBtn").disabled = !this.checked;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label htmlFor="kcRecoveryCodesConfirmationCheck">{msg("recovery-codes-confirmation-message")}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action={kcContext.url.loginAction} className={getClassName("kcFormGroupClass")} id="kc-recovery-codes-settings-form" method="post">
|
||||||
|
<input type="hidden" name="generatedRecoveryAuthnCodes" value={recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesAsString} />
|
||||||
|
<input type="hidden" name="generatedAt" value={recoveryAuthnCodesConfigBean.generatedAt} />
|
||||||
|
<input type="hidden" id="userLabel" name="userLabel" value={msgStr("recovery-codes-label-default")} />
|
||||||
|
|
||||||
|
<LogoutOtherSessions {...{ getClassName, i18n }} />
|
||||||
|
|
||||||
|
{isAppInitiatedAction ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonPrimaryClass"), getClassName("kcButtonLargeClass"))}
|
||||||
|
id="saveRecoveryAuthnCodesBtn"
|
||||||
|
value={msgStr("recovery-codes-action-complete")}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonDefaultClass"), getClassName("kcButtonLargeClass"))}
|
||||||
|
id="cancelRecoveryAuthnCodesBtn"
|
||||||
|
name="cancel-aia"
|
||||||
|
value="true"
|
||||||
|
>
|
||||||
|
{msg("recovery-codes-action-cancel")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonPrimaryClass"),
|
||||||
|
getClassName("kcButtonBlockClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
id="saveRecoveryAuthnCodesBtn"
|
||||||
|
value={msgStr("recovery-codes-action-complete")}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogoutOtherSessions(props: { getClassName: ReturnType<typeof useGetClassName>["getClassName"]; i18n: I18n }) {
|
||||||
|
const { getClassName, i18n } = props;
|
||||||
|
|
||||||
|
const { msg } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
||||||
|
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
||||||
|
<div className="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" defaultChecked={true} />
|
||||||
|
{msg("logoutOtherSessions")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user