mirror of
https://github.com/zadam/trilium.git
synced 2025-10-21 23:59:02 +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";
|
import { ComponentChildren } from "preact";
|
||||||
|
|
||||||
interface AlertProps {
|
interface AlertProps {
|
||||||
type: "info" | "danger";
|
type: "info" | "danger" | "warning";
|
||||||
title?: string;
|
title?: string;
|
||||||
children: ComponentChildren;
|
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 CodeEditorOptions from "./options/code_notes/code_editor.js";
|
||||||
import CodeAutoReadOnlySizeOptions from "./options/code_notes/code_auto_read_only_size.js";
|
import CodeAutoReadOnlySizeOptions from "./options/code_notes/code_auto_read_only_size.js";
|
||||||
import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.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 SearchEngineOptions from "./options/other/search_engine.js";
|
||||||
import TrayOptions from "./options/other/tray.js";
|
import TrayOptions from "./options/other/tray.js";
|
||||||
import NoteErasureTimeoutOptions from "./options/other/note_erasure_timeout.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 EtapiSettings from "./options/etapi.js";
|
||||||
import BackupSettings from "./options/backup.js";
|
import BackupSettings from "./options/backup.js";
|
||||||
import SpellcheckSettings from "./options/spellcheck.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">
|
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
|
||||||
<style>
|
<style>
|
||||||
@ -89,10 +88,7 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextA
|
|||||||
],
|
],
|
||||||
_optionsImages: <ImageSettings />,
|
_optionsImages: <ImageSettings />,
|
||||||
_optionsSpellcheck: <SpellcheckSettings />,
|
_optionsSpellcheck: <SpellcheckSettings />,
|
||||||
_optionsPassword: [
|
_optionsPassword: <PasswordSettings />,
|
||||||
PasswordOptions,
|
|
||||||
ProtectedSessionTimeoutOptions
|
|
||||||
],
|
|
||||||
_optionsMFA: [MultiFactorAuthenticationOptions],
|
_optionsMFA: [MultiFactorAuthenticationOptions],
|
||||||
_optionsEtapi: <EtapiSettings />,
|
_optionsEtapi: <EtapiSettings />,
|
||||||
_optionsBackup: <BackupSettings />,
|
_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 passwordService from "../../services/encryption/password.js";
|
||||||
import ValidationError from "../../errors/validation_error.js";
|
import ValidationError from "../../errors/validation_error.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
|
import { ChangePasswordResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
function changePassword(req: Request) {
|
function changePassword(req: Request): ChangePasswordResponse {
|
||||||
if (passwordService.isPasswordSet()) {
|
if (passwordService.isPasswordSet()) {
|
||||||
return passwordService.changePassword(req.body.current_password, req.body.new_password);
|
return passwordService.changePassword(req.body.current_password, req.body.new_password);
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,12 +3,13 @@ import optionService from "../options.js";
|
|||||||
import myScryptService from "./my_scrypt.js";
|
import myScryptService from "./my_scrypt.js";
|
||||||
import { randomSecureToken, toBase64 } from "../utils.js";
|
import { randomSecureToken, toBase64 } from "../utils.js";
|
||||||
import passwordEncryptionService from "./password_encryption.js";
|
import passwordEncryptionService from "./password_encryption.js";
|
||||||
|
import { ChangePasswordResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
function isPasswordSet() {
|
function isPasswordSet() {
|
||||||
return !!sql.getValue("SELECT value FROM options WHERE name = 'passwordVerificationHash'");
|
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()) {
|
if (!isPasswordSet()) {
|
||||||
throw new Error("Password has not been set yet, so it cannot be changed. Use 'setPassword' instead.");
|
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()) {
|
if (isPasswordSet()) {
|
||||||
throw new Error("Password is set already. Either change it or perform 'reset password' first.");
|
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 = {
|
type Response = {
|
||||||
success: true,
|
success: true,
|
||||||
message: string;
|
message?: string;
|
||||||
} | {
|
} | {
|
||||||
success: false;
|
success: false;
|
||||||
message: string;
|
message: string;
|
||||||
@ -107,3 +107,5 @@ export interface DatabaseBackup {
|
|||||||
filePath: string;
|
filePath: string;
|
||||||
mtime: Date;
|
mtime: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChangePasswordResponse = Response;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user