feat(react/settings): port change password

This commit is contained in:
Elian Doran 2025-08-15 14:18:59 +03:00
parent fb559d66fe
commit c02ed17ebc
No known key found for this signature in database
8 changed files with 132 additions and 134 deletions

View File

@ -1,7 +1,7 @@
import { ComponentChildren } from "preact";
interface AlertProps {
type: "info" | "danger";
type: "info" | "danger" | "warning";
title?: string;
children: ComponentChildren;
}

View File

@ -0,0 +1,17 @@
import { ComponentChild } from "preact";
interface LinkButtonProps {
onClick: () => void;
text: ComponentChild;
}
export default function LinkButton({ onClick, text }: LinkButtonProps) {
return (
<a class="tn-link" href="javascript:" onClick={(e) => {
e.preventDefault();
onClick();
}}>
{text}
</a>
)
}

View File

@ -8,8 +8,6 @@ import DateTimeFormatOptions from "./options/text_notes/date_time_format.js";
import CodeEditorOptions from "./options/code_notes/code_editor.js";
import CodeAutoReadOnlySizeOptions from "./options/code_notes/code_auto_read_only_size.js";
import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.js";
import PasswordOptions from "./options/password/password.js";
import ProtectedSessionTimeoutOptions from "./options/password/protected_session_timeout.js";
import SearchEngineOptions from "./options/other/search_engine.js";
import TrayOptions from "./options/other/tray.js";
import NoteErasureTimeoutOptions from "./options/other/note_erasure_timeout.js";
@ -40,6 +38,7 @@ import SyncOptions from "./options/sync.jsx";
import EtapiSettings from "./options/etapi.js";
import BackupSettings from "./options/backup.js";
import SpellcheckSettings from "./options/spellcheck.js";
import PasswordSettings from "./options/password.jsx";
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
<style>
@ -89,10 +88,7 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextA
],
_optionsImages: <ImageSettings />,
_optionsSpellcheck: <SpellcheckSettings />,
_optionsPassword: [
PasswordOptions,
ProtectedSessionTimeoutOptions
],
_optionsPassword: <PasswordSettings />,
_optionsMFA: [MultiFactorAuthenticationOptions],
_optionsEtapi: <EtapiSettings />,
_optionsBackup: <BackupSettings />,

View File

@ -0,0 +1,104 @@
import { useState } from "preact/hooks"
import { t } from "../../../services/i18n"
import server from "../../../services/server"
import toast from "../../../services/toast"
import Alert from "../../react/Alert"
import Button from "../../react/Button"
import FormGroup from "../../react/FormGroup"
import FormTextBox from "../../react/FormTextBox"
import LinkButton from "../../react/LinkButton"
import OptionsSection from "./components/OptionsSection"
import protected_session_holder from "../../../services/protected_session_holder"
import { ChangePasswordResponse } from "@triliumnext/commons"
import dialog from "../../../services/dialog"
import { reloadFrontendApp } from "../../../services/utils"
export default function PasswordSettings() {
return (
<>
<ChangePassword />
</>
)
}
function ChangePassword() {
const [ oldPassword, setOldPassword ] = useState("");
const [ newPassword1, setNewPassword1 ] = useState("");
const [ newPassword2, setNewPassword2 ] = useState("");
return (
<OptionsSection title={t("password.heading")}>
<Alert type="warning">
{t("password.alert_message")}
&nbsp;
<LinkButton
text={t("password.reset_link")}
onClick={async () => {
if (!confirm(t("password.reset_confirmation"))) {
return;
}
await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes");
toast.showError(t("password.reset_success_message"));
}}
/>
</Alert>
<form onSubmit={async (e) => {
e.preventDefault();
setOldPassword("");
setNewPassword1("");
setNewPassword2("");
if (newPassword1 !== newPassword2) {
toast.showError(t("password.password_mismatch"));
return;
}
const result = await server
.post<ChangePasswordResponse>("password/change", {
current_password: oldPassword,
new_password: newPassword1
})
if (result.success) {
await dialog.info(t("password.password_changed_success"));
// password changed so current protected session is invalid and needs to be cleared
protected_session_holder.resetProtectedSession();
} else if (result.message) {
toast.showError(result.message);
}
}}>
<FormGroup label={t("password.old_password")}>
<FormTextBox
name="old-password"
type="password"
currentValue={oldPassword} onChange={setOldPassword}
/>
</FormGroup>
<FormGroup label={t("password.new_password")}>
<FormTextBox
name="new-password1"
type="password"
currentValue={newPassword1} onChange={setNewPassword1}
/>
</FormGroup>
<FormGroup label={t("password.new_password_confirmation")}>
<FormTextBox
name="new-password2"
type="password"
currentValue={newPassword2} onChange={setNewPassword2}
/>
</FormGroup>
<Button
text={t("password.change_password")}
primary
/>
</form>
</OptionsSection>
)
}

View File

@ -1,123 +0,0 @@
import { t } from "../../../../services/i18n.js";
import server from "../../../../services/server.js";
import protectedSessionHolder from "../../../../services/protected_session_holder.js";
import toastService from "../../../../services/toast.js";
import OptionsWidget from "../options_widget.js";
import type { OptionMap } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="options-section">
<h4 class="password-heading">${t("password.heading")}</h4>
<div class="alert alert-warning" role="alert">
${t("password.alert_message")} <a class="reset-password-button tn-link" href="javascript:">${t("password.reset_link")}</a>
</div>
<form class="change-password-form">
<div class="old-password-form-group form-group">
<label for="old-password">${t("password.old_password")}</label>
<input id="old-password" class="old-password form-control" type="password">
</div>
<div class="form-group">
<label for="new-password1">${t("password.new_password")}</label>
<input id="new-password1" class="new-password1 form-control" type="password">
</div>
<div class="form-group">
<label for="new-password2">${t("password.new_password_confirmation")}</label>
<input id="new-password2" class="new-password2 form-control" type="password">
</div>
<button class="save-password-button btn btn-primary">${t("password.change_password")}</button>
</form>
</div>
`;
// TODO: Deduplicate
interface ChangePasswordResponse {
success: boolean;
message?: string;
}
export default class PasswordOptions extends OptionsWidget {
private $passwordHeading!: JQuery<HTMLElement>;
private $changePasswordForm!: JQuery<HTMLElement>;
private $oldPassword!: JQuery<HTMLElement>;
private $newPassword1!: JQuery<HTMLElement>;
private $newPassword2!: JQuery<HTMLElement>;
private $savePasswordButton!: JQuery<HTMLElement>;
private $resetPasswordButton!: JQuery<HTMLElement>;
private $protectedSessionTimeout!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$passwordHeading = this.$widget.find(".password-heading");
this.$changePasswordForm = this.$widget.find(".change-password-form");
this.$oldPassword = this.$widget.find(".old-password");
this.$newPassword1 = this.$widget.find(".new-password1");
this.$newPassword2 = this.$widget.find(".new-password2");
this.$savePasswordButton = this.$widget.find(".save-password-button");
this.$resetPasswordButton = this.$widget.find(".reset-password-button");
this.$resetPasswordButton.on("click", async () => {
if (confirm(t("password.reset_confirmation"))) {
await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes");
const options = await server.get<OptionMap>("options");
this.optionsLoaded(options);
toastService.showError(t("password.reset_success_message"));
}
});
this.$changePasswordForm.on("submit", () => this.save());
this.$protectedSessionTimeout = this.$widget.find(".protected-session-timeout-in-seconds");
this.$protectedSessionTimeout.on("change", () => this.updateOption("protectedSessionTimeout", this.$protectedSessionTimeout.val()));
}
optionsLoaded(options: OptionMap) {
const isPasswordSet = options.isPasswordSet === "true";
this.$widget.find(".old-password-form-group").toggle(isPasswordSet);
this.$passwordHeading.text(isPasswordSet ? t("password.change_password_heading") : t("password.set_password_heading"));
this.$savePasswordButton.text(isPasswordSet ? t("password.change_password") : t("password.set_password"));
this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
}
save() {
const oldPassword = this.$oldPassword.val();
const newPassword1 = this.$newPassword1.val();
const newPassword2 = this.$newPassword2.val();
this.$oldPassword.val("");
this.$newPassword1.val("");
this.$newPassword2.val("");
if (newPassword1 !== newPassword2) {
toastService.showError(t("password.password_mismatch"));
return false;
}
server
.post<ChangePasswordResponse>("password/change", {
current_password: oldPassword,
new_password: newPassword1
})
.then((result) => {
if (result.success) {
toastService.showError(t("password.password_changed_success"));
// password changed so current protected session is invalid and needs to be cleared
protectedSessionHolder.resetProtectedSession();
} else if (result.message) {
toastService.showError(result.message);
}
});
return false;
}
}

View File

@ -3,8 +3,9 @@
import passwordService from "../../services/encryption/password.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import { ChangePasswordResponse } from "@triliumnext/commons";
function changePassword(req: Request) {
function changePassword(req: Request): ChangePasswordResponse {
if (passwordService.isPasswordSet()) {
return passwordService.changePassword(req.body.current_password, req.body.new_password);
} else {

View File

@ -3,12 +3,13 @@ import optionService from "../options.js";
import myScryptService from "./my_scrypt.js";
import { randomSecureToken, toBase64 } from "../utils.js";
import passwordEncryptionService from "./password_encryption.js";
import { ChangePasswordResponse } from "@triliumnext/commons";
function isPasswordSet() {
return !!sql.getValue("SELECT value FROM options WHERE name = 'passwordVerificationHash'");
}
function changePassword(currentPassword: string, newPassword: string) {
function changePassword(currentPassword: string, newPassword: string): ChangePasswordResponse {
if (!isPasswordSet()) {
throw new Error("Password has not been set yet, so it cannot be changed. Use 'setPassword' instead.");
}
@ -41,7 +42,7 @@ function changePassword(currentPassword: string, newPassword: string) {
};
}
function setPassword(password: string) {
function setPassword(password: string): ChangePasswordResponse {
if (isPasswordSet()) {
throw new Error("Password is set already. Either change it or perform 'reset password' first.");
}

View File

@ -2,7 +2,7 @@ import { AttributeRow, NoteType } from "./rows.js";
type Response = {
success: true,
message: string;
message?: string;
} | {
success: false;
message: string;
@ -107,3 +107,5 @@ export interface DatabaseBackup {
filePath: string;
mtime: Date;
}
export type ChangePasswordResponse = Response;