mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 07:08:55 +02:00
feat(react/settings): port totp settings
This commit is contained in:
parent
73ff41f2b2
commit
cfb3607052
@ -25,6 +25,7 @@ export interface PromptDialogOptions {
|
||||
defaultValue?: string;
|
||||
shown?: PromptShownDialogCallback;
|
||||
callback?: (value: string | null) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
function PromptDialogComponent() {
|
||||
@ -77,7 +78,9 @@ function PromptDialogComponent() {
|
||||
<FormTextBox
|
||||
name="prompt-dialog-answer"
|
||||
inputRef={answerRef}
|
||||
currentValue={value} onChange={setValue} />
|
||||
currentValue={value} onChange={setValue}
|
||||
readOnly={opts.current?.readOnly}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
interface AdmonitionProps {
|
||||
type: "warning";
|
||||
type: "warning" | "note" | "caution";
|
||||
children: ComponentChildren;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import MultiFactorAuthenticationOptions from './options/multi_factor_authentication.js';
|
||||
import AiSettingsOptions from "./options/ai_settings.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
@ -21,7 +20,7 @@ import TextNoteSettings from "./options/text_notes.jsx";
|
||||
import CodeNoteSettings from "./options/code_notes.jsx";
|
||||
import OtherSettings from "./options/other.jsx";
|
||||
import BackendLogWidget from "./content/backend_log.js";
|
||||
import { unmountComponentAtNode } from "preact/compat";
|
||||
import MultiFactorAuthenticationSettings from "./options/multi_factor_authentication.js";
|
||||
|
||||
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
|
||||
<style>
|
||||
@ -56,7 +55,7 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextA
|
||||
_optionsImages: <ImageSettings />,
|
||||
_optionsSpellcheck: <SpellcheckSettings />,
|
||||
_optionsPassword: <PasswordSettings />,
|
||||
_optionsMFA: [MultiFactorAuthenticationOptions],
|
||||
_optionsMFA: <MultiFactorAuthenticationSettings />,
|
||||
_optionsEtapi: <EtapiSettings />,
|
||||
_optionsBackup: <BackupSettings />,
|
||||
_optionsSync: <SyncOptions />,
|
||||
|
@ -9,9 +9,9 @@ interface OptionsSectionProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function OptionsSection({ title, children, noCard, ...rest }: OptionsSectionProps) {
|
||||
export default function OptionsSection({ title, children, noCard, className, ...rest }: OptionsSectionProps) {
|
||||
return (
|
||||
<div className={`options-section ${noCard && "tn-no-card"}`} {...rest}>
|
||||
<div className={`options-section ${noCard && "tn-no-card"} ${className ?? ""}`} {...rest}>
|
||||
{title && <h4>{title}</h4>}
|
||||
{children}
|
||||
</div>
|
||||
|
@ -1,342 +0,0 @@
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsWidget from "./options_widget.js";
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import dialogService from "../../../services/dialog.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("multi_factor_authentication.title")}</h4>
|
||||
<p class="form-text">${t("multi_factor_authentication.description")}</p>
|
||||
|
||||
<div class="col-md-6 side-checkbox">
|
||||
<label class="form-check tn-checkbox">
|
||||
<input type="checkbox" class="mfa-enabled-checkbox form-check-input" />
|
||||
${t("multi_factor_authentication.mfa_enabled")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="mfa-options" style="display: none;">
|
||||
<label class="form-label"><b>${t("multi_factor_authentication.mfa_method")}</b></label>
|
||||
<div role="group">
|
||||
<label class="tn-radio">
|
||||
<input class="mfa-method-radio" type="radio" name="mfaMethod" value="totp" />
|
||||
<b>${t("multi_factor_authentication.totp_title")}</b>
|
||||
</label>
|
||||
<label class="tn-radio">
|
||||
<input class="mfa-method-radio" type="radio" name="mfaMethod" value="oauth" />
|
||||
<b>${t("multi_factor_authentication.oauth_title")}</b>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="totp-options" style="display: none;">
|
||||
<p class="form-text">${t("multi_factor_authentication.totp_description")}</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>${t("multi_factor_authentication.totp_secret_title")}</h5>
|
||||
<div class="admonition note no-totp-secret" role="alert">
|
||||
${t("multi_factor_authentication.no_totp_secret_warning")}
|
||||
</div>
|
||||
|
||||
<div class="admonition warning" role="alert">
|
||||
${t("multi_factor_authentication.totp_secret_description_warning")}
|
||||
</div>
|
||||
|
||||
<button class="generate-totp btn btn-primary">
|
||||
${t("multi_factor_authentication.totp_secret_generate")}
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>${t("multi_factor_authentication.recovery_keys_title")}</h5>
|
||||
<p class="form-text">${t("multi_factor_authentication.recovery_keys_description")}</p>
|
||||
<div class="admonition caution">
|
||||
${t("multi_factor_authentication.recovery_keys_description_warning")}
|
||||
</div>
|
||||
|
||||
<table style="border: 0px solid white">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="key_0"></td>
|
||||
<td style="width: 20px" />
|
||||
<td class="key_1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key_2"></td>
|
||||
<td />
|
||||
<td class="key_3"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key_4"></td>
|
||||
<td />
|
||||
<td class="key_5"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key_6"></td>
|
||||
<td />
|
||||
<td class="key_7"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<button class="generate-recovery-code btn btn-primary"> ${t("multi_factor_authentication.recovery_keys_generate")} </button>
|
||||
</div>
|
||||
|
||||
<div class="oauth-options" style="display: none;">
|
||||
<p class="form-text">${t("multi_factor_authentication.oauth_description")}</p>
|
||||
<div class="admonition note oauth-warning" role="alert">
|
||||
${t("multi_factor_authentication.oauth_description_warning")}
|
||||
</div>
|
||||
<div class="admonition caution missing-vars" role="alert" style="display: none;"></div>
|
||||
<hr />
|
||||
<div class="col-md-6">
|
||||
<span><b>${t("multi_factor_authentication.oauth_user_account")}</b></span><span class="user-account-name">${t("multi_factor_authentication.oauth_user_not_logged_in")}</span>
|
||||
<br>
|
||||
<span><b>${t("multi_factor_authentication.oauth_user_email")}</b></span><span class="user-account-email">${t("multi_factor_authentication.oauth_user_not_logged_in")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const TPL_ELECTRON = `
|
||||
<div class="options-section">
|
||||
<h4>${t("multi_factor_authentication.title")}</h4>
|
||||
<p class="form-text">${t("multi_factor_authentication.electron_disabled")}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
interface OAuthStatus {
|
||||
enabled: boolean;
|
||||
name?: string;
|
||||
email?: string;
|
||||
missingVars?: string[];
|
||||
}
|
||||
|
||||
interface TOTPStatus {
|
||||
set: boolean;
|
||||
}
|
||||
|
||||
interface RecoveryKeysResponse {
|
||||
success: boolean;
|
||||
recoveryCodes?: string[];
|
||||
keysExist?: boolean;
|
||||
usedRecoveryCodes?: string[];
|
||||
}
|
||||
|
||||
export default class MultiFactorAuthenticationOptions extends OptionsWidget {
|
||||
private $mfaEnabledCheckbox!: JQuery<HTMLElement>;
|
||||
private $mfaOptions!: JQuery<HTMLElement>;
|
||||
private $mfaMethodRadios!: JQuery<HTMLElement>;
|
||||
private $totpOptions!: JQuery<HTMLElement>;
|
||||
private $noTotpSecretWarning!: JQuery<HTMLElement>;
|
||||
private $generateTotpButton!: JQuery<HTMLElement>;
|
||||
private $generateRecoveryCodeButton!: JQuery<HTMLElement>;
|
||||
private $recoveryKeys: JQuery<HTMLElement>[] = [];
|
||||
private $oauthOptions!: JQuery<HTMLElement>;
|
||||
private $UserAccountName!: JQuery<HTMLElement>;
|
||||
private $UserAccountEmail!: JQuery<HTMLElement>;
|
||||
private $oauthWarning!: JQuery<HTMLElement>;
|
||||
private $missingVars!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
const template = utils.isElectron() ? TPL_ELECTRON : TPL;
|
||||
this.$widget = $(template);
|
||||
|
||||
if (!utils.isElectron()) {
|
||||
this.$mfaEnabledCheckbox = this.$widget.find(".mfa-enabled-checkbox");
|
||||
this.$mfaOptions = this.$widget.find(".mfa-options");
|
||||
this.$mfaMethodRadios = this.$widget.find(".mfa-method-radio");
|
||||
this.$totpOptions = this.$widget.find(".totp-options");
|
||||
this.$noTotpSecretWarning = this.$widget.find(".no-totp-secret");
|
||||
this.$generateTotpButton = this.$widget.find(".generate-totp");
|
||||
this.$generateRecoveryCodeButton = this.$widget.find(".generate-recovery-code");
|
||||
|
||||
this.$oauthOptions = this.$widget.find(".oauth-options");
|
||||
this.$UserAccountName = this.$widget.find(".user-account-name");
|
||||
this.$UserAccountEmail = this.$widget.find(".user-account-email");
|
||||
this.$oauthWarning = this.$widget.find(".oauth-warning");
|
||||
this.$missingVars = this.$widget.find(".missing-vars");
|
||||
|
||||
this.$recoveryKeys = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
this.$recoveryKeys.push(this.$widget.find(".key_" + i));
|
||||
}
|
||||
|
||||
this.$generateRecoveryCodeButton.on("click", async () => {
|
||||
await this.setRecoveryKeys();
|
||||
});
|
||||
|
||||
this.$generateTotpButton.on("click", async () => {
|
||||
await this.generateKey();
|
||||
});
|
||||
|
||||
this.displayRecoveryKeys();
|
||||
|
||||
this.$mfaEnabledCheckbox.on("change", () => {
|
||||
const isChecked = this.$mfaEnabledCheckbox.is(":checked");
|
||||
this.$mfaOptions.toggle(isChecked);
|
||||
if (!isChecked) {
|
||||
this.$totpOptions.hide();
|
||||
this.$oauthOptions.hide();
|
||||
} else {
|
||||
this.$mfaMethodRadios.filter('[value="totp"]').prop("checked", true);
|
||||
this.$totpOptions.show();
|
||||
this.$oauthOptions.hide();
|
||||
}
|
||||
this.updateCheckboxOption("mfaEnabled", this.$mfaEnabledCheckbox);
|
||||
});
|
||||
|
||||
this.$mfaMethodRadios.on("change", () => {
|
||||
const selectedMethod = this.$mfaMethodRadios.filter(":checked").val();
|
||||
this.$totpOptions.toggle(selectedMethod === "totp");
|
||||
this.$oauthOptions.toggle(selectedMethod === "oauth");
|
||||
this.updateOption("mfaMethod", selectedMethod);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setRecoveryKeys() {
|
||||
const result = await server.get<RecoveryKeysResponse>("totp_recovery/generate");
|
||||
if (!result.success) {
|
||||
toastService.showError(t("multi_factor_authentication.recovery_keys_error"));
|
||||
return;
|
||||
}
|
||||
if (result.recoveryCodes) {
|
||||
this.keyFiller(result.recoveryCodes);
|
||||
await server.post("totp_recovery/set", {
|
||||
recoveryCodes: result.recoveryCodes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async displayRecoveryKeys() {
|
||||
const result = await server.get<RecoveryKeysResponse>("totp_recovery/enabled");
|
||||
if (!result.success) {
|
||||
this.fillKeys(t("multi_factor_authentication.recovery_keys_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.keysExist) {
|
||||
this.fillKeys(t("multi_factor_authentication.recovery_keys_no_key_set"));
|
||||
this.$generateRecoveryCodeButton.text(t("multi_factor_authentication.recovery_keys_generate"));
|
||||
return;
|
||||
}
|
||||
|
||||
const usedResult = await server.get<RecoveryKeysResponse>("totp_recovery/used");
|
||||
|
||||
if (usedResult.usedRecoveryCodes) {
|
||||
this.keyFiller(usedResult.usedRecoveryCodes);
|
||||
this.$generateRecoveryCodeButton.text(t("multi_factor_authentication.recovery_keys_regenerate"));
|
||||
} else {
|
||||
this.fillKeys(t("multi_factor_authentication.recovery_keys_no_key_set"));
|
||||
}
|
||||
}
|
||||
|
||||
private keyFiller(values: string[]) {
|
||||
this.fillKeys("");
|
||||
|
||||
values.forEach((key, index) => {
|
||||
if (typeof key === 'string') {
|
||||
const date = new Date(key.replace(/\//g, '-'));
|
||||
if (isNaN(date.getTime())) {
|
||||
this.$recoveryKeys[index].text(key);
|
||||
} else {
|
||||
this.$recoveryKeys[index].text(t("multi_factor_authentication.recovery_keys_used", { date: key.replace(/\//g, '-') }));
|
||||
}
|
||||
} else {
|
||||
this.$recoveryKeys[index].text(t("multi_factor_authentication.recovery_keys_unused", { index: key }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fillKeys(message: string) {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
this.$recoveryKeys[i].text(message);
|
||||
}
|
||||
}
|
||||
|
||||
async generateKey() {
|
||||
const totpStatus = await server.get<TOTPStatus>("totp/status");
|
||||
|
||||
if (totpStatus.set) {
|
||||
const confirmed = await dialogService.confirm(t("multi_factor_authentication.totp_secret_regenerate_confirm"));
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await server.get<{ success: boolean; message: string }>("totp/generate");
|
||||
|
||||
if (result.success) {
|
||||
await dialogService.prompt({
|
||||
title: t("multi_factor_authentication.totp_secret_generated"),
|
||||
message: t("multi_factor_authentication.totp_secret_warning"),
|
||||
defaultValue: result.message,
|
||||
shown: ({ $answer }) => {
|
||||
if ($answer) {
|
||||
$answer.prop('readonly', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$generateTotpButton.text(t("multi_factor_authentication.totp_secret_regenerate"));
|
||||
|
||||
await this.setRecoveryKeys();
|
||||
} else {
|
||||
toastService.showError(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
optionsLoaded(options: OptionMap) {
|
||||
if (!utils.isElectron()) {
|
||||
this.$mfaEnabledCheckbox.prop("checked", options.mfaEnabled === "true");
|
||||
|
||||
this.$mfaOptions.toggle(options.mfaEnabled === "true");
|
||||
if (options.mfaEnabled === "true") {
|
||||
const savedMethod = options.mfaMethod || "totp";
|
||||
this.$mfaMethodRadios.filter(`[value="${savedMethod}"]`).prop("checked", true);
|
||||
this.$totpOptions.toggle(savedMethod === "totp");
|
||||
this.$oauthOptions.toggle(savedMethod === "oauth");
|
||||
} else {
|
||||
this.$totpOptions.hide();
|
||||
this.$oauthOptions.hide();
|
||||
}
|
||||
|
||||
server.get<OAuthStatus>("oauth/status").then((result) => {
|
||||
if (result.enabled) {
|
||||
if (result.name) this.$UserAccountName.text(result.name);
|
||||
if (result.email) this.$UserAccountEmail.text(result.email);
|
||||
this.$oauthWarning.hide();
|
||||
this.$missingVars.hide();
|
||||
} else {
|
||||
this.$UserAccountName.text(t("multi_factor_authentication.oauth_user_not_logged_in"));
|
||||
this.$UserAccountEmail.text(t("multi_factor_authentication.oauth_user_not_logged_in"));
|
||||
this.$oauthWarning.show();
|
||||
if (result.missingVars && result.missingVars.length > 0) {
|
||||
this.$missingVars.show();
|
||||
const missingVarsList = result.missingVars.map(v => `"${v}"`);
|
||||
this.$missingVars.html(t("multi_factor_authentication.oauth_missing_vars", { variables: missingVarsList.join(", ") }));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.get<TOTPStatus>("totp/status").then((result) => {
|
||||
if (result.set) {
|
||||
this.$generateTotpButton.text(t("multi_factor_authentication.totp_secret_regenerate"));
|
||||
this.$noTotpSecretWarning.hide();
|
||||
} else {
|
||||
this.$noTotpSecretWarning.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
import { Trans } from "react-i18next"
|
||||
import { t } from "../../../services/i18n"
|
||||
import FormText from "../../react/FormText"
|
||||
import OptionsSection from "./components/OptionsSection"
|
||||
import FormCheckbox from "../../react/FormCheckbox"
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"
|
||||
import FormGroup from "../../react/FormGroup"
|
||||
import { FormInlineRadioGroup } from "../../react/FormRadioGroup"
|
||||
import Admonition from "../../react/Admonition"
|
||||
import { useCallback, useEffect, useMemo, useState } from "preact/hooks"
|
||||
import { TOTPGenerate, TOTPRecoveryKeysResponse, TOTPStatus } from "@triliumnext/commons"
|
||||
import server from "../../../services/server"
|
||||
import Button from "../../react/Button"
|
||||
import dialog from "../../../services/dialog"
|
||||
import toast from "../../../services/toast"
|
||||
|
||||
export default function MultiFactorAuthenticationSettings() {
|
||||
const [ mfaEnabled, setMfaEnabled ] = useTriliumOptionBool("mfaEnabled");
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnableMultiFactor mfaEnabled={mfaEnabled} setMfaEnabled={setMfaEnabled} />
|
||||
<MultiFactorMethod />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function EnableMultiFactor({ mfaEnabled, setMfaEnabled }: { mfaEnabled: boolean, setMfaEnabled: (newValue: boolean) => Promise<void>}) {
|
||||
return (
|
||||
<OptionsSection title={t("multi_factor_authentication.title")}>
|
||||
<FormText><Trans i18nKey="multi_factor_authentication.description" /></FormText>
|
||||
|
||||
<FormCheckbox
|
||||
name="mfa-enabled"
|
||||
label={t("multi_factor_authentication.mfa_enabled")}
|
||||
currentValue={mfaEnabled} onChange={setMfaEnabled}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
|
||||
function MultiFactorMethod() {
|
||||
const [ mfaMethod, setMfaMethod ] = useTriliumOption("mfaMethod");
|
||||
|
||||
return (
|
||||
<>
|
||||
<OptionsSection className="mfa-options" title={t("multi_factor_authentication.mfa_method")}>
|
||||
<FormInlineRadioGroup
|
||||
name="mfaMethod"
|
||||
currentValue={mfaMethod} onChange={setMfaMethod}
|
||||
values={[
|
||||
{ value: "totp", label: t("multi_factor_authentication.totp_title") },
|
||||
{ value: "oauth", label: t("multi_factor_authentication.oauth_title") }
|
||||
]}
|
||||
/>
|
||||
|
||||
<FormText>
|
||||
{ mfaMethod === "totp"
|
||||
? t("multi_factor_authentication.totp_description")
|
||||
: ""}
|
||||
</FormText>
|
||||
</OptionsSection>
|
||||
|
||||
{ mfaMethod === "totp" && <TotpSettings />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function TotpSettings() {
|
||||
return (
|
||||
<div class="totp-options">
|
||||
<TotpGenerateSecret />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TotpGenerateSecret() {
|
||||
const [ totpStatus, setTotpStatus ] = useState<TOTPStatus>();
|
||||
const [ recoveryKeys, setRecoveryKeys ] = useState<string[]>();
|
||||
|
||||
const refreshTotpStatus = useCallback(() => {
|
||||
server.get<TOTPStatus>("totp/status").then(setTotpStatus);
|
||||
}, [])
|
||||
|
||||
const refreshRecoveryKeys = useCallback(async () => {
|
||||
const result = await server.get<TOTPRecoveryKeysResponse>("totp_recovery/enabled");
|
||||
|
||||
if (!result.success) {
|
||||
toast.showError(t("multi_factor_authentication.recovery_keys_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.keysExist) {
|
||||
setRecoveryKeys(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const usedResult = await server.get<TOTPRecoveryKeysResponse>("totp_recovery/used");
|
||||
setRecoveryKeys(usedResult.usedRecoveryCodes);
|
||||
}, []);
|
||||
|
||||
const generateRecoveryKeys = useCallback(async () => {
|
||||
const result = await server.get<TOTPRecoveryKeysResponse>("totp_recovery/generate");
|
||||
if (!result.success) {
|
||||
toast.showError(t("multi_factor_authentication.recovery_keys_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.recoveryCodes) {
|
||||
setRecoveryKeys(result.recoveryCodes);
|
||||
}
|
||||
|
||||
await server.post("totp_recovery/set", {
|
||||
recoveryCodes: result.recoveryCodes,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refreshTotpStatus();
|
||||
refreshRecoveryKeys();
|
||||
}, []);
|
||||
|
||||
return (<>
|
||||
<OptionsSection title={t("multi_factor_authentication.totp_secret_title")}>
|
||||
{totpStatus?.set
|
||||
? <Admonition type="warning">{t("multi_factor_authentication.totp_secret_description_warning")}</Admonition>
|
||||
: <Admonition type="note">{t("multi_factor_authentication.no_totp_secret_warning")}</Admonition>
|
||||
}
|
||||
|
||||
<Button
|
||||
text={totpStatus?.set
|
||||
? t("multi_factor_authentication.totp_secret_regenerate")
|
||||
: t("multi_factor_authentication.totp_secret_generate")}
|
||||
onClick={async () => {
|
||||
if (totpStatus?.set && !await dialog.confirm(t("multi_factor_authentication.totp_secret_regenerate_confirm"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await server.get<TOTPGenerate>("totp/generate");
|
||||
if (!result.success) {
|
||||
toast.showError(result.message);
|
||||
return;
|
||||
}
|
||||
|
||||
await dialog.prompt({
|
||||
title: t("multi_factor_authentication.totp_secret_generated"),
|
||||
message: t("multi_factor_authentication.totp_secret_warning"),
|
||||
defaultValue: result.message,
|
||||
readOnly: true
|
||||
});
|
||||
refreshTotpStatus();
|
||||
await generateRecoveryKeys();
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
|
||||
<TotpRecoveryKeys values={recoveryKeys} generateRecoveryKeys={generateRecoveryKeys} />
|
||||
</>)
|
||||
}
|
||||
|
||||
function TotpRecoveryKeys({ values, generateRecoveryKeys }: { values?: string[], generateRecoveryKeys: () => Promise<void> }) {
|
||||
return (
|
||||
<OptionsSection title={t("multi_factor_authentication.recovery_keys_title")}>
|
||||
<FormText>{t("multi_factor_authentication.recovery_keys_description")}</FormText>
|
||||
|
||||
{values ? (
|
||||
<>
|
||||
<Admonition type="caution">
|
||||
<Trans i18nKey={t("multi_factor_authentication.recovery_keys_description_warning")} />
|
||||
</Admonition>
|
||||
|
||||
<ol style={{ columnCount: 2 }}>
|
||||
{values.map(key => {
|
||||
let text = "";
|
||||
|
||||
if (typeof key === 'string') {
|
||||
const date = new Date(key.replace(/\//g, '-'));
|
||||
if (isNaN(date.getTime())) {
|
||||
return <li><code>{key}</code></li>
|
||||
} else {
|
||||
text = t("multi_factor_authentication.recovery_keys_used", { date: key.replace(/\//g, '-') });
|
||||
}
|
||||
} else {
|
||||
text = t("multi_factor_authentication.recovery_keys_unused", { index: key });
|
||||
}
|
||||
|
||||
return <li>{text}</li>
|
||||
})}
|
||||
</ol>
|
||||
</>
|
||||
) : (
|
||||
<p>{t("multi_factor_authentication.recovery_keys_no_key_set")}</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
text={!values
|
||||
? t("multi_factor_authentication.recovery_keys_generate")
|
||||
: t("multi_factor_authentication.recovery_keys_regenerate")
|
||||
}
|
||||
onClick={generateRecoveryKeys}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
@ -109,3 +109,19 @@ export interface DatabaseBackup {
|
||||
}
|
||||
|
||||
export type ChangePasswordResponse = Response;
|
||||
|
||||
export interface TOTPStatus {
|
||||
set: boolean;
|
||||
}
|
||||
|
||||
export interface TOTPGenerate {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TOTPRecoveryKeysResponse {
|
||||
success: boolean;
|
||||
recoveryCodes?: string[];
|
||||
keysExist?: boolean;
|
||||
usedRecoveryCodes?: string[];
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user