mirror of
https://github.com/zadam/trilium.git
synced 2025-10-21 15:49:00 +02:00
feat(react/settings): port change password
This commit is contained in:
parent
fb559d66fe
commit
c02ed17ebc
@ -1,7 +1,7 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
interface AlertProps {
|
||||
type: "info" | "danger";
|
||||
type: "info" | "danger" | "warning";
|
||||
title?: string;
|
||||
children: ComponentChildren;
|
||||
}
|
||||
|
17
apps/client/src/widgets/react/LinkButton.tsx
Normal file
17
apps/client/src/widgets/react/LinkButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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 />,
|
||||
|
104
apps/client/src/widgets/type_widgets/options/password.tsx
Normal file
104
apps/client/src/widgets/type_widgets/options/password.tsx
Normal 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")}
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user