feat(options/password): use modals for changing the password

This commit is contained in:
Elian Doran
2026-04-13 18:18:04 +03:00
parent 9ba1eeaf20
commit 9326751923
2 changed files with 133 additions and 89 deletions

View File

@@ -1491,7 +1491,11 @@
"new_password": "New password",
"new_password_confirmation": "New password confirmation",
"change_password": "Change password",
"protected_session_timeout": "Protected Session Timeout",
"change_password_description": "Update your current password",
"reset_password": "Reset password",
"reset_password_description": "Permanently lose access to protected notes",
"cancel": "Cancel",
"protected_session_timeout": "Protected Session",
"protected_session_timeout_description": "Time of inactivity before the session is cleared from browser memory. See",
"wiki": "wiki",
"for_more_info": "for more info.",

View File

@@ -1,18 +1,18 @@
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 OptionsRow from "./components/OptionsRow"
import protected_session_holder from "../../../services/protected_session_holder"
import { ChangePasswordResponse } from "@triliumnext/commons"
import dialog from "../../../services/dialog"
import TimeSelector from "./components/TimeSelector"
import { useState } from "preact/hooks";
import { createPortal } from "preact/compat";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import toast from "../../../services/toast";
import Button from "../../react/Button";
import FormGroup from "../../react/FormGroup";
import FormTextBox from "../../react/FormTextBox";
import OptionsSection from "./components/OptionsSection";
import OptionsRow, { OptionsRowWithButton } from "./components/OptionsRow";
import protected_session_holder from "../../../services/protected_session_holder";
import { ChangePasswordResponse } from "@triliumnext/commons";
import dialog from "../../../services/dialog";
import TimeSelector from "./components/TimeSelector";
import Modal from "../../react/Modal";
export default function PasswordSettings() {
return (
@@ -20,86 +20,125 @@ export default function PasswordSettings() {
<ChangePassword />
<ProtectedSessionTimeout />
</>
)
);
}
function ChangePassword() {
const [ oldPassword, setOldPassword ] = useState("");
const [ newPassword1, setNewPassword1 ] = useState("");
const [ newPassword2, setNewPassword2 ] = useState("");
const [showModal, setShowModal] = useState(false);
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;
}
<OptionsRowWithButton
label={t("password.change_password")}
description={t("password.change_password_description")}
icon="bx bx-chevron-right"
onClick={() => setShowModal(true)}
/>
await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes");
toast.showError(t("password.reset_success_message"));
}}
/>
</Alert>
<OptionsRowWithButton
label={t("password.reset_password")}
description={t("password.reset_password_description")}
icon="bx bx-chevron-right"
onClick={async () => {
if (!await dialog.confirm(t("password.reset_confirmation"))) {
return;
}
<form onSubmit={async (e) => {
e.preventDefault();
await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes");
toast.showError(t("password.reset_success_message"));
}}
/>
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 name="old-password" label={t("password.old_password")}>
<FormTextBox
type="password"
currentValue={oldPassword} onChange={setOldPassword}
/>
</FormGroup>
<FormGroup name="new-password1" label={t("password.new_password")}>
<FormTextBox
type="password"
currentValue={newPassword1} onChange={setNewPassword1}
/>
</FormGroup>
<FormGroup name="new-password2" label={t("password.new_password_confirmation")}>
<FormTextBox
type="password"
currentValue={newPassword2} onChange={setNewPassword2}
/>
</FormGroup>
<Button
text={t("password.change_password")}
kind="primary"
/>
</form>
{createPortal(
<ChangePasswordModal show={showModal} onHidden={() => setShowModal(false)} />,
document.body
)}
</OptionsSection>
)
);
}
interface ChangePasswordModalProps {
show: boolean;
onHidden: () => void;
}
function ChangePasswordModal({ show, onHidden }: ChangePasswordModalProps) {
const [oldPassword, setOldPassword] = useState("");
const [newPassword1, setNewPassword1] = useState("");
const [newPassword2, setNewPassword2] = useState("");
const handleSubmit = async () => {
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) {
onHidden();
setOldPassword("");
setNewPassword1("");
setNewPassword2("");
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);
}
};
const handleHidden = () => {
setOldPassword("");
setNewPassword1("");
setNewPassword2("");
onHidden();
};
return (
<Modal
show={show}
onHidden={handleHidden}
onSubmit={handleSubmit}
title={t("password.change_password_heading")}
className="change-password-modal"
size="md"
footer={
<>
<Button text={t("password.cancel")} onClick={handleHidden} />
<Button text={t("password.change_password")} kind="primary" />
</>
}
>
<FormGroup name="old-password" label={t("password.old_password")}>
<FormTextBox
type="password"
currentValue={oldPassword}
onChange={setOldPassword}
/>
</FormGroup>
<FormGroup name="new-password1" label={t("password.new_password")}>
<FormTextBox
type="password"
currentValue={newPassword1}
onChange={setNewPassword1}
/>
</FormGroup>
<FormGroup name="new-password2" label={t("password.new_password_confirmation")}>
<FormTextBox
type="password"
currentValue={newPassword2}
onChange={setNewPassword2}
/>
</FormGroup>
</Modal>
);
}
function ProtectedSessionTimeout() {
@@ -112,10 +151,11 @@ function ProtectedSessionTimeout() {
>
<TimeSelector
name="protected-session-timeout"
optionValueId="protectedSessionTimeout" optionTimeScaleId="protectedSessionTimeoutTimeScale"
optionValueId="protectedSessionTimeout"
optionTimeScaleId="protectedSessionTimeoutTimeScale"
minimumSeconds={60}
/>
</OptionsRow>
</OptionsSection>
)
}
);
}