mirror of
https://github.com/zadam/trilium.git
synced 2025-10-21 15:49:00 +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;
|
defaultValue?: string;
|
||||||
shown?: PromptShownDialogCallback;
|
shown?: PromptShownDialogCallback;
|
||||||
callback?: (value: string | null) => void;
|
callback?: (value: string | null) => void;
|
||||||
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PromptDialogComponent() {
|
function PromptDialogComponent() {
|
||||||
@ -77,7 +78,9 @@ function PromptDialogComponent() {
|
|||||||
<FormTextBox
|
<FormTextBox
|
||||||
name="prompt-dialog-answer"
|
name="prompt-dialog-answer"
|
||||||
inputRef={answerRef}
|
inputRef={answerRef}
|
||||||
currentValue={value} onChange={setValue} />
|
currentValue={value} onChange={setValue}
|
||||||
|
readOnly={opts.current?.readOnly}
|
||||||
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
|
|
||||||
interface AdmonitionProps {
|
interface AdmonitionProps {
|
||||||
type: "warning";
|
type: "warning" | "note" | "caution";
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import TypeWidget from "./type_widget.js";
|
import TypeWidget from "./type_widget.js";
|
||||||
import MultiFactorAuthenticationOptions from './options/multi_factor_authentication.js';
|
|
||||||
import AiSettingsOptions from "./options/ai_settings.js";
|
import AiSettingsOptions from "./options/ai_settings.js";
|
||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import type NoteContextAwareWidget from "../note_context_aware_widget.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 CodeNoteSettings from "./options/code_notes.jsx";
|
||||||
import OtherSettings from "./options/other.jsx";
|
import OtherSettings from "./options/other.jsx";
|
||||||
import BackendLogWidget from "./content/backend_log.js";
|
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">
|
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
|
||||||
<style>
|
<style>
|
||||||
@ -56,7 +55,7 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextA
|
|||||||
_optionsImages: <ImageSettings />,
|
_optionsImages: <ImageSettings />,
|
||||||
_optionsSpellcheck: <SpellcheckSettings />,
|
_optionsSpellcheck: <SpellcheckSettings />,
|
||||||
_optionsPassword: <PasswordSettings />,
|
_optionsPassword: <PasswordSettings />,
|
||||||
_optionsMFA: [MultiFactorAuthenticationOptions],
|
_optionsMFA: <MultiFactorAuthenticationSettings />,
|
||||||
_optionsEtapi: <EtapiSettings />,
|
_optionsEtapi: <EtapiSettings />,
|
||||||
_optionsBackup: <BackupSettings />,
|
_optionsBackup: <BackupSettings />,
|
||||||
_optionsSync: <SyncOptions />,
|
_optionsSync: <SyncOptions />,
|
||||||
|
@ -9,9 +9,9 @@ interface OptionsSectionProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OptionsSection({ title, children, noCard, ...rest }: OptionsSectionProps) {
|
export default function OptionsSection({ title, children, noCard, className, ...rest }: OptionsSectionProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`options-section ${noCard && "tn-no-card"}`} {...rest}>
|
<div className={`options-section ${noCard && "tn-no-card"} ${className ?? ""}`} {...rest}>
|
||||||
{title && <h4>{title}</h4>}
|
{title && <h4>{title}</h4>}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</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 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