feat(react/settings): port database anonymization

This commit is contained in:
Elian Doran 2025-08-14 23:10:25 +03:00
parent 7e03774b8e
commit 16cd91eb02
No known key found for this signature in database
6 changed files with 106 additions and 126 deletions

View File

@ -1,14 +1,16 @@
import type { ComponentChildren } from "preact"; import type { ComponentChildren } from "preact";
import { CSSProperties } from "preact/compat";
interface ColumnProps { interface ColumnProps {
md?: number; md?: number;
children: ComponentChildren; children: ComponentChildren;
className?: string; className?: string;
style?: CSSProperties;
} }
export default function Column({ md, children, className }: ColumnProps) { export default function Column({ md, children, className, style }: ColumnProps) {
return ( return (
<div className={`col-md-${md ?? 6} ${className ?? ""}`}> <div className={`col-md-${md ?? 6} ${className ?? ""}`} style={style}>
{children} {children}
</div> </div>
) )

View File

@ -21,8 +21,6 @@ import RevisionsSnapshotIntervalOptions from "./options/other/revisions_snapshot
import RevisionSnapshotsLimitOptions from "./options/other/revision_snapshots_limit.js"; import RevisionSnapshotsLimitOptions from "./options/other/revision_snapshots_limit.js";
import NetworkConnectionsOptions from "./options/other/network_connections.js"; import NetworkConnectionsOptions from "./options/other/network_connections.js";
import HtmlImportTagsOptions from "./options/other/html_import_tags.js"; import HtmlImportTagsOptions from "./options/other/html_import_tags.js";
import VacuumDatabaseOptions from "./options/advanced/vacuum_database.js";
import DatabaseAnonymizationOptions from "./options/advanced/database_anonymization.js";
import BackendLogWidget from "./content/backend_log.js"; import BackendLogWidget from "./content/backend_log.js";
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js"; import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
import RibbonOptions from "./options/appearance/ribbon.js"; import RibbonOptions from "./options/appearance/ribbon.js";

View File

@ -1,15 +1,18 @@
import { DatabaseCheckIntegrityResponse } from "@triliumnext/commons"; import { AnonymizedDbResponse, DatabaseAnonymizeResponse, DatabaseCheckIntegrityResponse } from "@triliumnext/commons";
import { t } from "../../../services/i18n"; import { t } from "../../../services/i18n";
import server from "../../../services/server"; import server from "../../../services/server";
import toast from "../../../services/toast"; import toast from "../../../services/toast";
import Button from "../../react/Button"; import Button from "../../react/Button";
import FormText from "../../react/FormText"; import FormText from "../../react/FormText";
import OptionsSection from "./components/OptionsSection" import OptionsSection from "./components/OptionsSection"
import Column from "../../react/Column";
import { useEffect, useState } from "preact/hooks";
export default function AdvancedSettings() { export default function AdvancedSettings() {
return <> return <>
<AdvancedSyncOptions /> <AdvancedSyncOptions />
<DatabaseIntegrityOptions /> <DatabaseIntegrityOptions />
<DatabaseAnonymizationOptions />
<VacuumDatabaseOptions /> <VacuumDatabaseOptions />
</>; </>;
} }
@ -69,6 +72,91 @@ function DatabaseIntegrityOptions() {
) )
} }
function DatabaseAnonymizationOptions() {
const [ existingAnonymizedDatabases, setExistingAnonymizedDatabases ] = useState<AnonymizedDbResponse[]>([]);
function refreshAnonymizedDatabase() {
server.get<AnonymizedDbResponse[]>("database/anonymized-databases").then(setExistingAnonymizedDatabases);
}
useEffect(refreshAnonymizedDatabase, []);
return (
<OptionsSection title={t("database_anonymization.title")}>
<FormText>{t("database_anonymization.choose_anonymization")}</FormText>
<div className="row">
<DatabaseAnonymizationOption
title={t("database_anonymization.full_anonymization")}
description={t("database_anonymization.full_anonymization_description")}
buttonText={t("database_anonymization.save_fully_anonymized_database")}
buttonClick={async () => {
toast.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/full");
if (!resp.success) {
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
} else {
toast.showMessage(t("database_anonymization.successfully_created_fully_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
refreshAnonymizedDatabase();
}
}}
/>
<DatabaseAnonymizationOption
title={t("database_anonymization.light_anonymization")}
description={t("database_anonymization.light_anonymization_description")}
buttonText={t("database_anonymization.save_lightly_anonymized_database")}
buttonClick={async () => {
toast.showMessage(t("database_anonymization.creating_lightly_anonymized_database"));
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/light");
if (!resp.success) {
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
} else {
toast.showMessage(t("database_anonymization.successfully_created_lightly_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
refreshAnonymizedDatabase();
}
}}
/>
</div>
<hr />
<ExistingAnonymizedDatabases databases={existingAnonymizedDatabases} />
</OptionsSection>
)
}
function DatabaseAnonymizationOption({ title, description, buttonText, buttonClick }: { title: string, description: string, buttonText: string, buttonClick: () => void }) {
return (
<Column md={6} style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginTop: "1em" }}>
<h5>{title}</h5>
<FormText>{description}</FormText>
<Button text={buttonText} onClick={buttonClick} />
</Column>
)
}
function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbResponse[] }) {
if (!databases.length) {
return <FormText>{t("database_anonymization.no_anonymized_database_yet")}</FormText>
}
return (
<table className="table table-stripped">
<thead>
<th>{t("database_anonymization.existing_anonymized_databases")}</th>
</thead>
<tbody>
{databases.map(({ filePath }) => (
<tr>
<td>{filePath}</td>
</tr>
))}
</tbody>
</table>
)
}
function VacuumDatabaseOptions() { function VacuumDatabaseOptions() {
return ( return (
<OptionsSection title={t("vacuum_database.title")}> <OptionsSection title={t("vacuum_database.title")}>

View File

@ -1,119 +0,0 @@
import OptionsWidget from "../options_widget.js";
import toastService from "../../../../services/toast.js";
import server from "../../../../services/server.js";
import { t } from "../../../../services/i18n.js";
import type { OptionMap } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="options-section">
<style>
.database-database-anonymization-option {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: 1em;
}
.database-database-anonymization-option p {
margin-top: .75em;
flex-grow: 1;
}
</style>
<h4>${t("database_anonymization.title")}</h4>
<div class="row">
<p class="form-text">${t("database_anonymization.choose_anonymization")}</p>
<div class="col-md-6 database-database-anonymization-option">
<h5>${t("database_anonymization.full_anonymization")}</h5>
<p class="form-text">${t("database_anonymization.full_anonymization_description")}</p>
<button class="anonymize-full-button btn btn-secondary">${t("database_anonymization.save_fully_anonymized_database")}</button>
</div>
<div class="col-md-6 database-database-anonymization-option">
<h5>${t("database_anonymization.light_anonymization")}</h5>
<p class="form-text">${t("database_anonymization.light_anonymization_description")}</p>
<button class="anonymize-light-button btn btn-secondary">${t("database_anonymization.save_lightly_anonymized_database")}</button>
</div>
</div>
<hr />
<table class="existing-anonymized-databases-table table table-stripped">
<thead>
<th>${t("database_anonymization.existing_anonymized_databases")}</th>
</thead>
<tbody class="existing-anonymized-databases">
</tbody>
</table>
</div>`;
// TODO: Deduplicate with server
interface AnonymizeResponse {
success: boolean;
anonymizedFilePath: string;
}
interface AnonymizedDbResponse {
filePath: string;
}
export default class DatabaseAnonymizationOptions extends OptionsWidget {
private $anonymizeFullButton!: JQuery<HTMLElement>;
private $anonymizeLightButton!: JQuery<HTMLElement>;
private $existingAnonymizedDatabases!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$anonymizeFullButton = this.$widget.find(".anonymize-full-button");
this.$anonymizeLightButton = this.$widget.find(".anonymize-light-button");
this.$anonymizeFullButton.on("click", async () => {
toastService.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
const resp = await server.post<AnonymizeResponse>("database/anonymize/full");
if (!resp.success) {
toastService.showError(t("database_anonymization.error_creating_anonymized_database"));
} else {
toastService.showMessage(t("database_anonymization.successfully_created_fully_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
}
this.refresh();
});
this.$anonymizeLightButton.on("click", async () => {
toastService.showMessage(t("database_anonymization.creating_lightly_anonymized_database"));
const resp = await server.post<AnonymizeResponse>("database/anonymize/light");
if (!resp.success) {
toastService.showError(t("database_anonymization.error_creating_anonymized_database"));
} else {
toastService.showMessage(t("database_anonymization.successfully_created_lightly_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
}
this.refresh();
});
this.$existingAnonymizedDatabases = this.$widget.find(".existing-anonymized-databases");
}
optionsLoaded(options: OptionMap) {
server.get<AnonymizedDbResponse[]>("database/anonymized-databases").then((anonymizedDatabases) => {
this.$existingAnonymizedDatabases.empty();
if (!anonymizedDatabases.length) {
anonymizedDatabases = [{ filePath: t("database_anonymization.no_anonymized_database_yet") }];
}
for (const { filePath } of anonymizedDatabases) {
this.$existingAnonymizedDatabases.append($("<tr>").append($("<td>").text(filePath)));
}
});
}
}

View File

@ -5,6 +5,7 @@ import dateUtils from "./date_utils.js";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import sql from "./sql.js"; import sql from "./sql.js";
import path from "path"; import path from "path";
import { AnonymizedDbResponse, DatabaseAnonymizeResponse } from "@triliumnext/commons";
function getFullAnonymizationScript() { function getFullAnonymizationScript() {
// we want to delete all non-builtin attributes because they can contain sensitive names and values // we want to delete all non-builtin attributes because they can contain sensitive names and values
@ -73,7 +74,7 @@ async function createAnonymizedCopy(type: "full" | "light") {
return { return {
success: true, success: true,
anonymizedFilePath: anonymizedFile anonymizedFilePath: anonymizedFile
}; } satisfies DatabaseAnonymizeResponse;
} }
function getExistingAnonymizedDatabases() { function getExistingAnonymizedDatabases() {
@ -87,7 +88,7 @@ function getExistingAnonymizedDatabases() {
.map((fileName) => ({ .map((fileName) => ({
fileName: fileName, fileName: fileName,
filePath: path.resolve(dataDir.ANONYMIZED_DB_DIR, fileName) filePath: path.resolve(dataDir.ANONYMIZED_DB_DIR, fileName)
})); })) satisfies AnonymizedDbResponse[];
} }
export default { export default {

View File

@ -67,3 +67,13 @@ export interface DatabaseCheckIntegrityResponse {
integrity_check: string; integrity_check: string;
}[]; }[];
} }
export interface DatabaseAnonymizeResponse {
success: boolean;
anonymizedFilePath: string;
}
export interface AnonymizedDbResponse {
filePath: string;
fileName: string;
}