diff --git a/src/bin/keycloakify/generateFtl/pageId.ts b/src/bin/keycloakify/generateFtl/pageId.ts index 94a336ff..2dc5c7f7 100644 --- a/src/bin/keycloakify/generateFtl/pageId.ts +++ b/src/bin/keycloakify/generateFtl/pageId.ts @@ -29,7 +29,8 @@ export const loginThemePageIds = [ "delete-credential.ftl", "code.ftl", "delete-account-confirm.ftl", - "frontchannel-logout.ftl" + "frontchannel-logout.ftl", + "login-recovery-authn-code-config.ftl" ] as const; export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl", "totp.ftl", "applications.ftl", "log.ftl"] as const; diff --git a/src/login/Fallback.tsx b/src/login/Fallback.tsx index 94b24dc7..4a6ad13d 100644 --- a/src/login/Fallback.tsx +++ b/src/login/Fallback.tsx @@ -35,6 +35,7 @@ const DeleteCredential = lazy(() => import("keycloakify/login/pages/DeleteCreden const Code = lazy(() => import("keycloakify/login/pages/Code")); const DeleteAccountConfirm = lazy(() => import("keycloakify/login/pages/DeleteAccountConfirm")); const FrontchannelLogout = lazy(() => import("keycloakify/login/pages/FrontchannelLogout")); +const LoginRecoveryAuthnCodeConfig = lazy(() => import("keycloakify/login/pages/LoginRecoveryAuthnCodeConfig")); type FallbackProps = PageProps & { UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>; @@ -107,6 +108,8 @@ export default function Fallback(props: FallbackProps) { return ; case "frontchannel-logout.ftl": return ; + case "login-recovery-authn-code-config.ftl": + return ; } assert>(false); })()} diff --git a/src/login/kcContext/KcContext.ts b/src/login/kcContext/KcContext.ts index 5885e997..50e77844 100644 --- a/src/login/kcContext/KcContext.ts +++ b/src/login/kcContext/KcContext.ts @@ -39,7 +39,8 @@ export type KcContext = | KcContext.DeleteCredential | KcContext.Code | KcContext.DeleteAccountConfirm - | KcContext.FrontchannelLogout; + | KcContext.FrontchannelLogout + | KcContext.LoginRecoveryAuthnCodeConfig; assert(); @@ -528,6 +529,15 @@ export declare namespace KcContext { logoutRedirectUri?: string; }; }; + + export type LoginRecoveryAuthnCodeConfig = Common & { + pageId: "login-recovery-authn-code-config.ftl"; + recoveryAuthnCodesConfigBean: { + generatedRecoveryAuthnCodesList: string[]; + generatedRecoveryAuthnCodesAsString: string; + generatedAt: string; + }; + }; } export type UserProfile = { diff --git a/src/login/pages/LoginRecoveryAuthnCodeConfig.tsx b/src/login/pages/LoginRecoveryAuthnCodeConfig.tsx new file mode 100644 index 00000000..c71dfbda --- /dev/null +++ b/src/login/pages/LoginRecoveryAuthnCodeConfig.tsx @@ -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, 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 = + "" + + "kc-download-recovery-codes" + + "

${msgStr("recovery-codes-download-file-header")}

" + + "
" + recoveryCodeListHTML + "
" + + "

${msgStr("recovery-codes-download-file-description")}

" + + "

${msgStr("recovery-codes-download-file-date")} " + formatCurrentDateTime() + "

" + + ""; + } + + 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 ( + + ); +} + +function LogoutOtherSessions(props: { getClassName: ReturnType["getClassName"]; i18n: I18n }) { + const { getClassName, i18n } = props; + + const { msg } = i18n; + + return ( +
+
+
+ +
+
+
+ ); +}