Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/web/__e2e__/models/settings-view.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,15 @@ export class SettingsViewModel {
.locator("input");
await titleFormatInput.fill(format);
}

async toggleKeepVaultNotesUnlocked() {
const item = await this.navigation.findItem("Vault");
await item?.click();

const keepVaultNotesUnlocked = this.page
.locator(getTestId("setting-keep-note-unlocked"))
.locator("label");

await keepVaultNotesUnlocked.click();
}
}
39 changes: 39 additions & 0 deletions apps/web/__e2e__/vault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,42 @@ test("clicking on vault unlocked status should lock the readonly note", async ({

expect(await note?.isLockedNotePasswordFieldVisible()).toBe(true);
});

test("when keep note unlocked setting is disabled (default), locked note should be locked again when it is closed after opening", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);

await note?.contextMenu.lock(PASSWORD);
await note?.openLockedNote(PASSWORD);
const tabs = await notes.editor.getTabs();
await tabs[0].close();

await note?.click();

expect(await note?.isLockedNotePasswordFieldVisible()).toBe(true);
});

test("when keep note unlocked setting is enabled, locked note should not be locked again when it is closed after opening", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);

await note?.contextMenu.lock(PASSWORD);
const settings = await app.goToSettings();
await settings.toggleKeepVaultNotesUnlocked();
await settings.close();
await note?.openLockedNote(PASSWORD);
const tabs = await notes.editor.getTabs();
await tabs[0].close();

await note?.click();

expect(await note?.isLockedNotePasswordFieldVisible()).toBe(false);
});
2 changes: 2 additions & 0 deletions apps/web/src/common/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { showPasswordDialog } from "../dialogs/password-dialog";
import { showToast } from "../utils/toast";
import { VAULT_ERRORS } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useStore as useAppStore } from "../stores/app-store";

class Vault {
static async createVault() {
Expand All @@ -34,6 +35,7 @@ class Vault {
},
validate: async ({ password }) => {
await db.vault.create(password);
useAppStore.getState().setIsVaultCreated(true);
showToast("success", strings.vaultCreated());
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/status-bar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ function StatusBar() {
}}
data-test-id="vault-unlocked"
>
<Unlock size={10} />
<Unlock size={12} />
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
{strings.vaultUnlocked()}
</Text>
Expand Down
21 changes: 18 additions & 3 deletions apps/web/src/dialogs/settings/vault-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,29 @@ export const VaultSettings: SettingsGroup[] = [
type: "button",
title: strings.create(),
action: () => {
Vault.createVault().then((res) => {
useAppStore.getState().setIsVaultCreated(res);
});
Vault.createVault();
},
variant: "secondary"
}
]
},
{
key: "keep-note-unlocked",
title: strings.keepNoteUnlocked(),
description: strings.keepNoteUnlockedDesc(),
isHidden: () => !useAppStore.getState().isVaultCreated,
onStateChange: (listener) =>
useAppStore.subscribe((s) => s.isVaultCreated, listener),
components: [
{
type: "toggle",
isToggled: () => useAppStore.getState().keepVaultNotesUnlocked,
toggle: () => {
useAppStore.getState().toggleKeepVaultNotesUnlocked();
}
}
]
},
{
key: "change-vault-password",
title: strings.changeVaultPassword(),
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/stores/app-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class AppStore extends BaseStore<AppStore> {
isListPaneVisible = true;
isNavPaneCollapsed = false;
isVaultCreated = false;
keepVaultNotesUnlocked = Config.get("vault:keepNotesUnlocked", false);
isAutoSyncEnabled = Config.get("autoSyncEnabled", true);
isSyncEnabled = Config.get("syncEnabled", true);
isRealtimeSyncEnabled = Config.get("isRealtimeSyncEnabled", true);
Expand Down Expand Up @@ -373,6 +374,12 @@ class AppStore extends BaseStore<AppStore> {
setNavigationTab = (tab: NavigationTabItem["id"]) => {
this.set((state) => (state.navigationTab = tab));
};

toggleKeepVaultNotesUnlocked = () => {
const newValue = !this.get().keepVaultNotesUnlocked;
Config.set("vault:keepNotesUnlocked", newValue);
this.set({ keepVaultNotesUnlocked: newValue });
};
}

const [useStore, store] = createStore<AppStore>(
Expand Down
41 changes: 31 additions & 10 deletions apps/web/src/stores/editor-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,16 +777,37 @@ class EditorStore extends BaseStore<EditorStore> {
options
);
} else if (isLocked && note.type !== "trash") {
this.addSession(
{
type: "locked",
id: sessionId,
note,
activeBlockId: options.activeBlockId,
tabId
},
options
);
if (
appStore.get().keepVaultNotesUnlocked &&
db.vault.isNoteOpened(note.id)
) {
const tags = await db.notes.tags(note.id);
const noteFromVault = await db.vault.open(note.id);
if (noteFromVault) {
this.addSession({
type: note.readonly ? "readonly" : "default",
locked: true,
id: sessionId,
note: noteFromVault,
saveState: SaveState.Saved,
sessionId: `${Date.now()}`,
tags: tags,
tabId: tabId,
content: noteFromVault.content
});
}
} else {
this.addSession(
{
type: "locked",
id: sessionId,
note,
activeBlockId: options.activeBlockId,
tabId
},
options
);
}
} else {
const content = note.contentId
? await db.content.get(note.contentId)
Expand Down
10 changes: 10 additions & 0 deletions docs/help/contents/lock-notes-with-private-vault.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ Adding notes to private vault is useful when you do not want anyone to read your

To open, edit or delete a locked note, you must provide the password for the vault to unlock it.

## Keep note unlocked when it is opened

By default, when you open a locked note, it will be locked again after you close the note or lock the vault. You can change this behavior, so that the note remains unlocked even after you close it. Now the note will only be locked when the vault is locked.

# [Desktop/Web](#/tab/web)

1. Go to Settings
2. Go to `Vault` in `Security & privacy` section
3. Toggle `Keep note unlocked`

## Unlock a note permanently

# [Desktop/Web](#/tab/web)
Expand Down
6 changes: 6 additions & 0 deletions packages/core/__tests__/vault.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ test("lock a note", () =>
expect(content.data.cipher).toBeDefined();

expect(await db.relations.from(vault, "note").has(id)).toBe(true);
expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));

test("locked note is not favorited", () =>
Expand All @@ -106,6 +107,7 @@ test("unlock a note", () =>
expect(note.content.data).toBeDefined();
expect(note.content.type).toBe(TEST_NOTE.content.type);
expect(await db.relations.from(vault, "note").has(id)).toBe(true);
expect(db.vault.isNoteOpened(id)).toBeTruthy();
}));

test("unlock a note permanently", () =>
Expand All @@ -124,6 +126,7 @@ test("unlock a note permanently", () =>
expect(content.data).toBeDefined();
expect(typeof content.data).toBe("string");
expect(await db.relations.from(vault, "note").has(id)).toBe(false);
expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));

test("lock an empty note", () =>
Expand All @@ -141,6 +144,7 @@ test("lock an empty note", () =>
expect(content.data.iv).toBeDefined();
expect(content.data.cipher).toBeDefined();
expect(await db.relations.from(vault, "note").has(id)).toBe(true);
expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));

test("save a locked note", () =>
Expand All @@ -154,6 +158,7 @@ test("save a locked note", () =>
const content = await db.content.get(note.contentId);

expect(content.data.cipher).toBeTypeOf("string");
expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));

test("save an edited locked note", () =>
Expand All @@ -173,6 +178,7 @@ test("save an edited locked note", () =>
expect(() => JSON.parse(content.data.cipher)).toThrow();
expect(note.dateEdited).toBeLessThan((await db.notes.note(id)).dateEdited);
expect(note.dateEdited).toBeLessThan(content.dateEdited);
expect(db.vault.isNoteOpened(id)).toBeFalsy();
}));

test("change vault password", () =>
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/api/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { EV, EVENTS } from "../common.js";
import { isCipher } from "../utils/crypto.js";
import { Note, NoteContent } from "../types.js";
import { logger } from "../logger.js";
import { addItem } from "../utils/array.js";

export const VAULT_ERRORS = {
noVault: "ERR_NO_VAULT",
Expand All @@ -35,6 +36,7 @@ export default class Vault {
private vaultPassword?: string;
private erasureTimeout = 0;
private key = "svvaads1212#2123";
private openedNotes: string[] = [];

private get password() {
return this.vaultPassword;
Expand Down Expand Up @@ -66,6 +68,10 @@ export default class Vault {
return !!this.vaultPassword;
}

isNoteOpened(noteId: string) {
return this.openedNotes.includes(noteId);
}

async create(password: string) {
const vaultKey = await this.getKey();
if (!vaultKey || !isCipher(vaultKey)) {
Expand All @@ -80,6 +86,7 @@ export default class Vault {

async lock() {
this.password = undefined;
this.openedNotes = [];
EV.publish(EVENTS.vaultLocked);
return true;
}
Expand Down Expand Up @@ -202,6 +209,8 @@ export default class Vault {
false
);

addItem(this.openedNotes, noteId);

if (password) {
this.password = password;
if (!(await this.exists())) await this.create(password);
Expand Down
8 changes: 8 additions & 0 deletions packages/intl/locale/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -3510,6 +3510,14 @@ msgstr "Jump to group"
msgid "Keep"
msgstr "Keep"

#: src/strings.ts:2639
msgid "Keep a vault note unlocked once it is opened. Note will be locked back once vault is locked."
msgstr "Keep a vault note unlocked once it is opened. Note will be locked back once vault is locked."

#: src/strings.ts:2637
msgid "Keep note unlocked"
msgstr "Keep note unlocked"

#: src/strings.ts:2021
msgid "Keep open"
msgstr "Keep open"
Expand Down
8 changes: 8 additions & 0 deletions packages/intl/locale/pseudo-LOCALE.po
Original file line number Diff line number Diff line change
Expand Up @@ -3490,6 +3490,14 @@ msgstr ""
msgid "Keep"
msgstr ""

#: src/strings.ts:2639
msgid "Keep a vault note unlocked once it is opened. Note will be locked back once vault is locked."
msgstr ""

#: src/strings.ts:2637
msgid "Keep note unlocked"
msgstr ""

#: src/strings.ts:2021
msgid "Keep open"
msgstr ""
Expand Down
5 changes: 4 additions & 1 deletion packages/intl/src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2633,5 +2633,8 @@ Use this if changes from other devices are not appearing on this device. This wi
exportCsv: () => t`Export CSV`,
importCsv: () => t`Import CSV`,
noContent: () => t`This note is empty`,
deleteData: () => t`Delete data`
deleteData: () => t`Delete data`,
keepNoteUnlocked: () => t`Keep note unlocked`,
keepNoteUnlockedDesc: () =>
t`Keep a vault note unlocked once it is opened. Note will be locked back once vault is locked.`
};
Loading