ETAPI is a REST API used to access Trilium instance programmatically, without UI.
+ See more details on wiki and ETAPI OpenAPI spec .
+
+There are no tokens yet. Click on the button above to create one.
+
+")
+ .append($("").text(token.name))
+ .append($(" ").text(token.utcDateCreated))
+ .append($(" ").append(
+ $(' ')
+ .on("click", () => this.renameToken(token.etapiTokenId, token.name)),
+ $(' ')
+ .on("click", () => this.deleteToken(token.etapiTokenId, token.name))
+ ))
+ );
+ }
+ }
+
+ async renameToken(etapiTokenId, oldName) {
+ const promptDialog = await import('../../dialogs/prompt.js');
+ const tokenName = await promptDialog.ask({
+ title: "Rename token",
+ message: "Please enter new token's name",
+ defaultValue: oldName
+ });
+
+ await server.patch(`etapi-tokens/${etapiTokenId}`, {name: tokenName});
+
+ this.refreshTokens();
+ }
+
+ async deleteToken(etapiTokenId, name) {
+ if (!confirm(`Are you sure you want to delete ETAPI token "${name}"?`)) {
+ return;
+ }
+
+ await server.remove(`etapi-tokens/${etapiTokenId}`);
+
+ this.refreshTokens();
+ }
+}
diff --git a/src/public/app/dialogs/options/credentials.js b/src/public/app/dialogs/options/password.js
similarity index 60%
rename from src/public/app/dialogs/options/credentials.js
rename to src/public/app/dialogs/options/password.js
index ce8c1a85c..471d1deb1 100644
--- a/src/public/app/dialogs/options/credentials.js
+++ b/src/public/app/dialogs/options/password.js
@@ -3,18 +3,16 @@ import protectedSessionHolder from "../../services/protected_session_holder.js";
import toastService from "../../services/toast.js";
const TPL = `
-Username
-
-Your username is .
-
-Change password
+
- Please take care to remember your new password. Password is used to encrypt protected notes. If you forget your password, then all your protected notes are forever lost with no recovery options.
+ Please take care to remember your new password. Password is used to encrypt protected notes.
+ If you forget your password, then all your protected notes are forever lost.
+ In case you did forget your password,
click here to reset it .
`;
export default class ChangePasswordOptions {
constructor() {
- $("#options-credentials").html(TPL);
+ $("#options-password").html(TPL);
- this.$username = $("#credentials-username");
+ this.$passwordHeading = $("#password-heading");
this.$form = $("#change-password-form");
this.$oldPassword = $("#old-password");
this.$newPassword1 = $("#new-password1");
this.$newPassword2 = $("#new-password2");
+ this.$savePasswordButton = $("#save-password-button");
+ this.$resetPasswordButton = $("#reset-password-button");
+
+ this.$resetPasswordButton.on("click", async () => {
+ if (confirm("By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?")) {
+ await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes");
+
+ const options = await server.get('options');
+ this.optionsLoaded(options);
+
+ alert("Password has been reset. Please set new password");
+ }
+ });
this.$form.on('submit', () => this.save());
}
optionsLoaded(options) {
- this.$username.text(options.username);
+ const isPasswordSet = options.isPasswordSet === 'true';
+
+ $("#old-password-form-group").toggle(isPasswordSet);
+ this.$passwordHeading.text(isPasswordSet ? 'Change password' : 'Set password');
+ this.$savePasswordButton.text(isPasswordSet ? 'Change password' : 'Set password');
}
save() {
diff --git a/src/public/app/dialogs/password_not_set.js b/src/public/app/dialogs/password_not_set.js
new file mode 100644
index 000000000..88bcbe6d5
--- /dev/null
+++ b/src/public/app/dialogs/password_not_set.js
@@ -0,0 +1,13 @@
+import utils from "../services/utils.js";
+import appContext from "../services/app_context.js";
+
+export function show() {
+ const $dialog = $("#password-not-set-dialog");
+ const $openPasswordOptionsButton = $("#open-password-options-button");
+
+ utils.openDialog($dialog);
+
+ $openPasswordOptionsButton.on("click", () => {
+ appContext.triggerCommand("showOptions", { openTab: 'password' });
+ });
+}
diff --git a/src/public/app/dialogs/prompt.js b/src/public/app/dialogs/prompt.js
index 34acfdaa5..c4e03b6b1 100644
--- a/src/public/app/dialogs/prompt.js
+++ b/src/public/app/dialogs/prompt.js
@@ -11,9 +11,11 @@ const $form = $("#prompt-dialog-form");
let resolve;
let shownCb;
-export function ask({ message, defaultValue, shown }) {
+export function ask({ title, message, defaultValue, shown }) {
shownCb = shown;
-
+
+ $("#prompt-title").text(title || "Prompt");
+
$question = $("")
.prop("for", "prompt-dialog-answer")
.text(message);
@@ -30,7 +32,7 @@ export function ask({ message, defaultValue, shown }) {
.append($question)
.append($answer));
- utils.openDialog($dialog);
+ utils.openDialog($dialog, false);
return new Promise((res, rej) => { resolve = res; });
}
diff --git a/src/public/app/dialogs/protected_session.js b/src/public/app/dialogs/protected_session.js
index a4822b8cf..3902cf299 100644
--- a/src/public/app/dialogs/protected_session.js
+++ b/src/public/app/dialogs/protected_session.js
@@ -12,7 +12,7 @@ export function show() {
}
export function close() {
- // this may fal if the dialog has not been previously opened (not sure if still true with Bootstrap modal)
+ // this may fail if the dialog has not been previously opened (not sure if still true with Bootstrap modal)
try {
$dialog.modal('hide');
}
diff --git a/src/public/app/services/app_context.js b/src/public/app/services/app_context.js
index f48ecef4f..a84477da6 100644
--- a/src/public/app/services/app_context.js
+++ b/src/public/app/services/app_context.js
@@ -118,7 +118,7 @@ class AppContext extends Component {
const appContext = new AppContext(window.glob.isMainWindow);
// we should save all outstanding changes before the page/app is closed
-$(window).on('beforeunload', () => {
+$(window).on('beforeunload', () => {return "SSS";
let allSaved = true;
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref());
diff --git a/src/public/app/services/attribute_renderer.js b/src/public/app/services/attribute_renderer.js
index 09eb698ff..913aeb1d2 100644
--- a/src/public/app/services/attribute_renderer.js
+++ b/src/public/app/services/attribute_renderer.js
@@ -81,6 +81,7 @@ async function renderAttributes(attributes, renderIsInheritable) {
const HIDDEN_ATTRIBUTES = [
'originalFileName',
+ 'fileSize',
'template',
'cssClass',
'iconClass',
diff --git a/src/public/app/services/date_notes.js b/src/public/app/services/date_notes.js
index d63de54bb..bfe83c3f6 100644
--- a/src/public/app/services/date_notes.js
+++ b/src/public/app/services/date_notes.js
@@ -11,12 +11,12 @@ async function getInboxNote() {
/** @returns {NoteShort} */
async function getTodayNote() {
- return await getDateNote(dayjs().format("YYYY-MM-DD"));
+ return await getDayNote(dayjs().format("YYYY-MM-DD"));
}
/** @returns {NoteShort} */
-async function getDateNote(date) {
- const note = await server.get('special-notes/date/' + date, "date-note");
+async function getDayNote(date) {
+ const note = await server.get('special-notes/days/' + date, "date-note");
await ws.waitForMaxKnownEntityChangeId();
@@ -25,7 +25,7 @@ async function getDateNote(date) {
/** @returns {NoteShort} */
async function getWeekNote(date) {
- const note = await server.get('special-notes/week/' + date, "date-note");
+ const note = await server.get('special-notes/weeks/' + date, "date-note");
await ws.waitForMaxKnownEntityChangeId();
@@ -34,7 +34,7 @@ async function getWeekNote(date) {
/** @returns {NoteShort} */
async function getMonthNote(month) {
- const note = await server.get('special-notes/month/' + month, "date-note");
+ const note = await server.get('special-notes/months/' + month, "date-note");
await ws.waitForMaxKnownEntityChangeId();
@@ -43,7 +43,7 @@ async function getMonthNote(month) {
/** @returns {NoteShort} */
async function getYearNote(year) {
- const note = await server.get('special-notes/year/' + year, "date-note");
+ const note = await server.get('special-notes/years/' + year, "date-note");
await ws.waitForMaxKnownEntityChangeId();
@@ -71,7 +71,7 @@ async function createSearchNote(opts = {}) {
export default {
getInboxNote,
getTodayNote,
- getDateNote,
+ getDayNote,
getWeekNote,
getMonthNote,
getYearNote,
diff --git a/src/public/app/services/froca_updater.js b/src/public/app/services/froca_updater.js
index 7c7640872..17347704b 100644
--- a/src/public/app/services/froca_updater.js
+++ b/src/public/app/services/froca_updater.js
@@ -22,9 +22,9 @@ async function processEntityChanges(entityChanges) {
} else if (ec.entityName === 'note_contents') {
delete froca.noteComplementPromises[ec.entityId];
- loadResults.addNoteContent(ec.entityId, ec.sourceId);
+ loadResults.addNoteContent(ec.entityId, ec.componentId);
} else if (ec.entityName === 'note_revisions') {
- loadResults.addNoteRevision(ec.entityId, ec.noteId, ec.sourceId);
+ loadResults.addNoteRevision(ec.entityId, ec.noteId, ec.componentId);
} else if (ec.entityName === 'note_revision_contents') {
// this should change only when toggling isProtected, ignore
} else if (ec.entityName === 'options') {
@@ -36,6 +36,9 @@ async function processEntityChanges(entityChanges) {
loadResults.addOption(ec.entity.name);
}
+ else if (ec.entityName === 'etapi_tokens') {
+ // NOOP
+ }
else {
throw new Error(`Unknown entityName ${ec.entityName}`);
}
@@ -87,7 +90,7 @@ function processNoteChange(loadResults, ec) {
return;
}
- loadResults.addNote(ec.entityId, ec.sourceId);
+ loadResults.addNote(ec.entityId, ec.componentId);
if (ec.isErased && ec.entityId in froca.notes) {
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
@@ -125,7 +128,7 @@ function processBranchChange(loadResults, ec) {
delete parentNote.childToBranch[branch.noteId];
}
- loadResults.addBranch(ec.entityId, ec.sourceId);
+ loadResults.addBranch(ec.entityId, ec.componentId);
delete froca.branches[ec.entityId];
}
@@ -133,7 +136,7 @@ function processBranchChange(loadResults, ec) {
return;
}
- loadResults.addBranch(ec.entityId, ec.sourceId);
+ loadResults.addBranch(ec.entityId, ec.componentId);
const childNote = froca.notes[ec.entity.noteId];
const parentNote = froca.notes[ec.entity.parentNoteId];
@@ -175,7 +178,7 @@ function processNoteReordering(loadResults, ec) {
}
}
- loadResults.addNoteReordering(ec.entityId, ec.sourceId);
+ loadResults.addNoteReordering(ec.entityId, ec.componentId);
}
function processAttributeChange(loadResults, ec) {
@@ -199,7 +202,7 @@ function processAttributeChange(loadResults, ec) {
targetNote.targetRelations = targetNote.targetRelations.filter(attributeId => attributeId !== attribute.attributeId);
}
- loadResults.addAttribute(ec.entityId, ec.sourceId);
+ loadResults.addAttribute(ec.entityId, ec.componentId);
delete froca.attributes[ec.entityId];
}
@@ -207,7 +210,7 @@ function processAttributeChange(loadResults, ec) {
return;
}
- loadResults.addAttribute(ec.entityId, ec.sourceId);
+ loadResults.addAttribute(ec.entityId, ec.componentId);
const sourceNote = froca.notes[ec.entity.noteId];
const targetNote = ec.entity.type === 'relation' && froca.notes[ec.entity.value];
diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js
index db8bc2d07..bcb8b5301 100644
--- a/src/public/app/services/frontend_script_api.js
+++ b/src/public/app/services/frontend_script_api.js
@@ -389,16 +389,26 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
this.getTodayNote = dateNotesService.getTodayNote;
/**
- * Returns date-note. If it doesn't exist, it is automatically created.
+ * Returns day note for a given date. If it doesn't exist, it is automatically created.
+ *
+ * @method
+ * @param {string} date - e.g. "2019-04-29"
+ * @return {Promise}
+ * @deprecated use getDayNote instead
+ */
+ this.getDateNote = dateNotesService.getDayNote;
+
+ /**
+ * Returns day note for a given date. If it doesn't exist, it is automatically created.
*
* @method
* @param {string} date - e.g. "2019-04-29"
* @return {Promise}
*/
- this.getDateNote = dateNotesService.getDateNote;
+ this.getDayNote = dateNotesService.getDayNote;
/**
- * Returns date-note for the first date of the week of the given date. If it doesn't exist, it is automatically created.
+ * Returns day note for the first date of the week of the given date. If it doesn't exist, it is automatically created.
*
* @method
* @param {string} date - e.g. "2019-04-29"
diff --git a/src/public/app/services/library_loader.js b/src/public/app/services/library_loader.js
index 8fb53549c..b6f0dbc8e 100644
--- a/src/public/app/services/library_loader.js
+++ b/src/public/app/services/library_loader.js
@@ -3,16 +3,17 @@ const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
const CODE_MIRROR = {
js: [
"libraries/codemirror/codemirror.js",
- "libraries/codemirror/addon/mode/loadmode.js",
- "libraries/codemirror/addon/mode/simple.js",
- "libraries/codemirror/addon/fold/xml-fold.js",
+ "libraries/codemirror/addon/display/placeholder.js",
"libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.js",
+ "libraries/codemirror/addon/fold/xml-fold.js",
+ "libraries/codemirror/addon/lint/lint.js",
+ "libraries/codemirror/addon/lint/eslint.js",
+ "libraries/codemirror/addon/mode/loadmode.js",
+ "libraries/codemirror/addon/mode/simple.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",
- "libraries/codemirror/keymap/vim.js",
- "libraries/codemirror/addon/lint/lint.js",
- "libraries/codemirror/addon/lint/eslint.js"
+ "libraries/codemirror/keymap/vim.js"
],
css: [
"libraries/codemirror/codemirror.css",
diff --git a/src/public/app/services/load_results.js b/src/public/app/services/load_results.js
index 6be2fef6a..036bc0e06 100644
--- a/src/public/app/services/load_results.js
+++ b/src/public/app/services/load_results.js
@@ -9,8 +9,8 @@ export default class LoadResults {
}
}
- this.noteIdToSourceId = {};
- this.sourceIdToNoteIds = {};
+ this.noteIdToComponentId = {};
+ this.componentIdToNoteIds = {};
this.branches = [];
@@ -20,7 +20,7 @@ export default class LoadResults {
this.noteRevisions = [];
- this.contentNoteIdToSourceId = [];
+ this.contentNoteIdToComponentId = [];
this.options = [];
}
@@ -29,22 +29,22 @@ export default class LoadResults {
return this.entities[entityName]?.[entityId];
}
- addNote(noteId, sourceId) {
- this.noteIdToSourceId[noteId] = this.noteIdToSourceId[noteId] || [];
+ addNote(noteId, componentId) {
+ this.noteIdToComponentId[noteId] = this.noteIdToComponentId[noteId] || [];
- if (!this.noteIdToSourceId[noteId].includes(sourceId)) {
- this.noteIdToSourceId[noteId].push(sourceId);
+ if (!this.noteIdToComponentId[noteId].includes(componentId)) {
+ this.noteIdToComponentId[noteId].push(componentId);
}
- this.sourceIdToNoteIds[sourceId] = this.sourceIdToNoteIds[sourceId] || [];
+ this.componentIdToNoteIds[componentId] = this.componentIdToNoteIds[componentId] || [];
- if (!this.sourceIdToNoteIds[sourceId]) {
- this.sourceIdToNoteIds[sourceId].push(noteId);
+ if (!this.componentIdToNoteIds[componentId]) {
+ this.componentIdToNoteIds[componentId].push(noteId);
}
}
- addBranch(branchId, sourceId) {
- this.branches.push({branchId, sourceId});
+ addBranch(branchId, componentId) {
+ this.branches.push({branchId, componentId});
}
getBranches() {
@@ -53,7 +53,7 @@ export default class LoadResults {
.filter(branch => !!branch);
}
- addNoteReordering(parentNoteId, sourceId) {
+ addNoteReordering(parentNoteId, componentId) {
this.noteReorderings.push(parentNoteId);
}
@@ -61,20 +61,20 @@ export default class LoadResults {
return this.noteReorderings;
}
- addAttribute(attributeId, sourceId) {
- this.attributes.push({attributeId, sourceId});
+ addAttribute(attributeId, componentId) {
+ this.attributes.push({attributeId, componentId});
}
/** @returns {Attribute[]} */
- getAttributes(sourceId = 'none') {
+ getAttributes(componentId = 'none') {
return this.attributes
- .filter(row => row.sourceId !== sourceId)
+ .filter(row => row.componentId !== componentId)
.map(row => this.getEntity("attributes", row.attributeId))
.filter(attr => !!attr);
}
- addNoteRevision(noteRevisionId, noteId, sourceId) {
- this.noteRevisions.push({noteRevisionId, noteId, sourceId});
+ addNoteRevision(noteRevisionId, noteId, componentId) {
+ this.noteRevisions.push({noteRevisionId, noteId, componentId});
}
hasNoteRevisionForNote(noteId) {
@@ -82,28 +82,28 @@ export default class LoadResults {
}
getNoteIds() {
- return Object.keys(this.noteIdToSourceId);
+ return Object.keys(this.noteIdToComponentId);
}
- isNoteReloaded(noteId, sourceId = null) {
+ isNoteReloaded(noteId, componentId = null) {
if (!noteId) {
return false;
}
- const sourceIds = this.noteIdToSourceId[noteId];
- return sourceIds && !!sourceIds.find(sId => sId !== sourceId);
+ const componentIds = this.noteIdToComponentId[noteId];
+ return componentIds && !!componentIds.find(sId => sId !== componentId);
}
- addNoteContent(noteId, sourceId) {
- this.contentNoteIdToSourceId.push({noteId, sourceId});
+ addNoteContent(noteId, componentId) {
+ this.contentNoteIdToComponentId.push({noteId, componentId});
}
- isNoteContentReloaded(noteId, sourceId) {
+ isNoteContentReloaded(noteId, componentId) {
if (!noteId) {
return false;
}
- return this.contentNoteIdToSourceId.find(l => l.noteId === noteId && l.sourceId !== sourceId);
+ return this.contentNoteIdToComponentId.find(l => l.noteId === noteId && l.componentId !== componentId);
}
addOption(name) {
@@ -124,17 +124,17 @@ export default class LoadResults {
}
isEmpty() {
- return Object.keys(this.noteIdToSourceId).length === 0
+ return Object.keys(this.noteIdToComponentId).length === 0
&& this.branches.length === 0
&& this.attributes.length === 0
&& this.noteReorderings.length === 0
&& this.noteRevisions.length === 0
- && this.contentNoteIdToSourceId.length === 0
+ && this.contentNoteIdToComponentId.length === 0
&& this.options.length === 0;
}
isEmptyForTree() {
- return Object.keys(this.noteIdToSourceId).length === 0
+ return Object.keys(this.noteIdToComponentId).length === 0
&& this.branches.length === 0
&& this.attributes.length === 0
&& this.noteReorderings.length === 0;
diff --git a/src/public/app/services/note_context.js b/src/public/app/services/note_context.js
index d2b4b4fb5..7d1c9bf3f 100644
--- a/src/public/app/services/note_context.js
+++ b/src/public/app/services/note_context.js
@@ -218,6 +218,13 @@ class NoteContext extends Component {
}
}
}
+
+ hasNoteList() {
+ return this.note.hasChildren()
+ && ['book', 'text', 'code'].includes(this.note.type)
+ && this.note.mime !== 'text/x-sqlite;schema=trilium'
+ && !this.note.hasLabel('hideChildrenOverview');
+ }
}
export default NoteContext;
diff --git a/src/public/app/services/protected_session.js b/src/public/app/services/protected_session.js
index 120841183..8b5a33745 100644
--- a/src/public/app/services/protected_session.js
+++ b/src/public/app/services/protected_session.js
@@ -5,6 +5,7 @@ import ws from "./ws.js";
import appContext from "./app_context.js";
import froca from "./froca.js";
import utils from "./utils.js";
+import options from "./options.js";
let protectedSessionDeferred = null;
@@ -18,6 +19,11 @@ async function leaveProtectedSession() {
function enterProtectedSession() {
const dfd = $.Deferred();
+ if (!options.is("isPasswordSet")) {
+ import("../dialogs/password_not_set.js").then(dialog => dialog.show());
+ return dfd;
+ }
+
if (protectedSessionHolder.isProtectedSessionAvailable()) {
dfd.resolve(false);
}
diff --git a/src/public/app/services/root_command_executor.js b/src/public/app/services/root_command_executor.js
index d3ac6f049..4aaf9d70c 100644
--- a/src/public/app/services/root_command_executor.js
+++ b/src/public/app/services/root_command_executor.js
@@ -53,8 +53,8 @@ export default class RootCommandExecutor extends Component {
d.showDialog(branchIds);
}
- showOptionsCommand() {
- import("../dialogs/options.js").then(d => d.showDialog());
+ showOptionsCommand({openTab}) {
+ import("../dialogs/options.js").then(d => d.showDialog(openTab));
}
showHelpCommand() {
diff --git a/src/public/app/services/server.js b/src/public/app/services/server.js
index 295936d12..e0c7d07dd 100644
--- a/src/public/app/services/server.js
+++ b/src/public/app/services/server.js
@@ -9,7 +9,7 @@ async function getHeaders(headers) {
// headers need to be lowercase because node.js automatically converts them to lower case
// also avoiding using underscores instead of dashes since nginx filters them out by default
const allHeaders = {
- 'trilium-source-id': glob.sourceId,
+ 'trilium-component-id': glob.componentId,
'trilium-local-now-datetime': utils.localNowDateTime(),
'trilium-hoisted-note-id': activeNoteContext ? activeNoteContext.hoistedNoteId : null,
'x-csrf-token': glob.csrfToken
@@ -29,20 +29,24 @@ async function getHeaders(headers) {
return allHeaders;
}
-async function get(url, sourceId) {
- return await call('GET', url, null, {'trilium-source-id': sourceId});
+async function get(url, componentId) {
+ return await call('GET', url, null, {'trilium-component-id': componentId});
}
-async function post(url, data, sourceId) {
- return await call('POST', url, data, {'trilium-source-id': sourceId});
+async function post(url, data, componentId) {
+ return await call('POST', url, data, {'trilium-component-id': componentId});
}
-async function put(url, data, sourceId) {
- return await call('PUT', url, data, {'trilium-source-id': sourceId});
+async function put(url, data, componentId) {
+ return await call('PUT', url, data, {'trilium-component-id': componentId});
}
-async function remove(url, sourceId) {
- return await call('DELETE', url, null, {'trilium-source-id': sourceId});
+async function patch(url, data, componentId) {
+ return await call('PATCH', url, data, {'trilium-component-id': componentId});
+}
+
+async function remove(url, componentId) {
+ return await call('DELETE', url, null, {'trilium-component-id': componentId});
}
let i = 1;
@@ -185,6 +189,7 @@ export default {
get,
post,
put,
+ patch,
remove,
ajax,
// don't remove, used from CKEditor image upload!
diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.js
index 462277021..213787b10 100644
--- a/src/public/app/services/utils.js
+++ b/src/public/app/services/utils.js
@@ -245,10 +245,11 @@ function focusSavedElement() {
$lastFocusedElement = null;
}
-async function openDialog($dialog) {
- closeActiveDialog();
-
- glob.activeDialog = $dialog;
+async function openDialog($dialog, closeActDialog = true) {
+ if (closeActDialog) {
+ closeActiveDialog();
+ glob.activeDialog = $dialog;
+ }
saveFocusedElement();
diff --git a/src/public/app/setup.js b/src/public/app/setup.js
index 288b69eb3..12d300b4f 100644
--- a/src/public/app/setup.js
+++ b/src/public/app/setup.js
@@ -19,20 +19,23 @@ function SetupModel() {
this.setupSyncFromDesktop = ko.observable(false);
this.setupSyncFromServer = ko.observable(false);
- this.username = ko.observable();
- this.password1 = ko.observable();
- this.password2 = ko.observable();
-
- this.theme = ko.observable("light");
this.syncServerHost = ko.observable();
this.syncProxy = ko.observable();
-
- this.instanceType = utils.isElectron() ? "desktop" : "server";
+ this.password = ko.observable();
this.setupTypeSelected = () => !!this.setupType();
this.selectSetupType = () => {
- this.step(this.setupType());
+ if (this.setupType() === 'new-document') {
+ this.step('new-document-in-progress');
+
+ $.post('api/setup/new-document').then(() => {
+ window.location.replace("./setup");
+ });
+ }
+ else {
+ this.step(this.setupType());
+ }
};
this.back = () => {
@@ -42,77 +45,36 @@ function SetupModel() {
};
this.finish = async () => {
- if (this.setupType() === 'new-document') {
- const username = this.username();
- const password1 = this.password1();
- const password2 = this.password2();
- const theme = this.theme();
+ const syncServerHost = this.syncServerHost();
+ const syncProxy = this.syncProxy();
+ const password = this.password();
- if (!username) {
- showAlert("Username can't be empty");
- return;
- }
-
- if (!password1) {
- showAlert("Password can't be empty");
- return;
- }
-
- if (password1 !== password2) {
- showAlert("Both password fields need be identical.");
- return;
- }
-
- this.step('new-document-in-progress');
-
- // not using server.js because it loads too many dependencies
- $.post('api/setup/new-document', {
- username: username,
- password: password1,
- theme: theme
- }).then(() => {
- window.location.replace("./setup");
- });
+ if (!syncServerHost) {
+ showAlert("Trilium server address can't be empty");
+ return;
}
- else if (this.setupType() === 'sync-from-server') {
- const syncServerHost = this.syncServerHost();
- const syncProxy = this.syncProxy();
- const username = this.username();
- const password = this.password1();
- if (!syncServerHost) {
- showAlert("Trilium server address can't be empty");
- return;
- }
+ if (!password) {
+ showAlert("Password can't be empty");
+ return;
+ }
- if (!username) {
- showAlert("Username can't be empty");
- return;
- }
+ // not using server.js because it loads too many dependencies
+ const resp = await $.post('api/setup/sync-from-server', {
+ syncServerHost: syncServerHost,
+ syncProxy: syncProxy,
+ password: password
+ });
- if (!password) {
- showAlert("Password can't be empty");
- return;
- }
+ if (resp.result === 'success') {
+ this.step('sync-in-progress');
- // not using server.js because it loads too many dependencies
- const resp = await $.post('api/setup/sync-from-server', {
- syncServerHost: syncServerHost,
- syncProxy: syncProxy,
- username: username,
- password: password
- });
+ setInterval(checkOutstandingSyncs, 1000);
- if (resp.result === 'success') {
- this.step('sync-in-progress');
-
- setInterval(checkOutstandingSyncs, 1000);
-
- hideAlert();
- }
- else {
- showAlert('Sync setup failed: ' + resp.error);
- }
+ hideAlert();
+ }
+ else {
+ showAlert('Sync setup failed: ' + resp.error);
}
};
}
diff --git a/src/public/app/widgets/buttons/calendar.js b/src/public/app/widgets/buttons/calendar.js
index 85d541c95..d7a396a8e 100644
--- a/src/public/app/widgets/buttons/calendar.js
+++ b/src/public/app/widgets/buttons/calendar.js
@@ -55,7 +55,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
this.$dropdownContent.on('click', '.calendar-date', async ev => {
const date = $(ev.target).closest('.calendar-date').attr('data-calendar-date');
- const note = await dateNoteService.getDateNote(date);
+ const note = await dateNoteService.getDayNote(date);
if (note) {
appContext.tabManager.getActiveContext().setNote(note.noteId);
diff --git a/src/public/app/widgets/containers/root_container.js b/src/public/app/widgets/containers/root_container.js
index 583d63027..da6d24e61 100644
--- a/src/public/app/widgets/containers/root_container.js
+++ b/src/public/app/widgets/containers/root_container.js
@@ -38,7 +38,7 @@ export default class RootContainer extends FlexContainer {
entitiesReloadedEvent({loadResults}) {
const note = appContext.tabManager.getActiveContextNote();
-
+
if (note && loadResults.isNoteReloaded(note.noteId)) {
this.refresh();
}
diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js
index e951fbbdc..39533183c 100644
--- a/src/public/app/widgets/note_detail.js
+++ b/src/public/app/widgets/note_detail.js
@@ -29,6 +29,10 @@ const TPL = `
font-family: var(--detail-font-family);
font-size: var(--detail-font-size);
}
+
+ .note-detail.full-height {
+ height: 100%;
+ }
`;
@@ -128,7 +132,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
await typeWidget.handleEvent('setNoteContext', {noteContext: this.noteContext});
- // this is happening in update() so note has been already set and we need to reflect this
+ // this is happening in update() so note has been already set, and we need to reflect this
await typeWidget.handleEvent('noteSwitched', {
noteContext: this.noteContext,
notePath: this.noteContext.notePath
@@ -136,6 +140,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
this.child(typeWidget);
}
+
+ this.checkFullHeight();
+ }
+
+ checkFullHeight() {
+ // https://github.com/zadam/trilium/issues/2522
+ this.$widget.toggleClass("full-height",
+ !this.noteContext.hasNoteList()
+ && ['editable-text', 'editable-code'].includes(this.type));
}
getTypeWidget() {
diff --git a/src/public/app/widgets/note_list.js b/src/public/app/widgets/note_list.js
index 785786e92..25aa22774 100644
--- a/src/public/app/widgets/note_list.js
+++ b/src/public/app/widgets/note_list.js
@@ -5,8 +5,6 @@ const TPL = `
@@ -105,7 +107,8 @@ export default class EditableCodeTypeWidget extends TypeWidget {
// we linewrap partly also because without it horizontal scrollbar displays only when you scroll
// all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem
lineWrapping: true,
- dragDrop: false // with true the editor inlines dropped files which is not what we expect
+ dragDrop: false, // with true the editor inlines dropped files which is not what we expect
+ placeholder: "Type the content of your code note here..."
});
this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate());
diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js
index 8a0209676..e82ba6d68 100644
--- a/src/public/app/widgets/type_widgets/editable_text.js
+++ b/src/public/app/widgets/type_widgets/editable_text.js
@@ -36,6 +36,7 @@ const TPL = `
font-family: var(--detail-font-family);
padding-left: 14px;
padding-top: 10px;
+ height: 100%;
}
.note-detail-editable-text a:hover {
@@ -73,6 +74,7 @@ const TPL = `
border: 0 !important;
box-shadow: none !important;
min-height: 50px;
+ height: 100%;
}
diff --git a/src/public/app/widgets/type_widgets/relation_map.js b/src/public/app/widgets/type_widgets/relation_map.js
index 83c77d9ce..f965a4c01 100644
--- a/src/public/app/widgets/type_widgets/relation_map.js
+++ b/src/public/app/widgets/type_widgets/relation_map.js
@@ -219,6 +219,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
else if (command === "editTitle") {
const promptDialog = await import("../../dialogs/prompt.js");
const title = await promptDialog.ask({
+ title: "Rename note",
message: "Enter new note title:",
defaultValue: $title.text()
});
diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css
index 42a5b5c93..5fac6f97c 100644
--- a/src/public/stylesheets/style.css
+++ b/src/public/stylesheets/style.css
@@ -241,6 +241,10 @@ body .CodeMirror {
background-color: #eeeeee
}
+.CodeMirror pre.CodeMirror-placeholder {
+ color: #999 !important;
+}
+
#sql-console-query {
height: 150px;
width: 100%;
diff --git a/src/routes/api/clipper.js b/src/routes/api/clipper.js
index 489229a4b..db6a2068f 100644
--- a/src/routes/api/clipper.js
+++ b/src/routes/api/clipper.js
@@ -36,7 +36,7 @@ function getClipperInboxNote() {
let clipperInbox = attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) {
- clipperInbox = dateNoteService.getDateNote(dateUtils.localNowDate());
+ clipperInbox = dateNoteService.getDayNote(dateUtils.localNowDate());
}
return clipperInbox;
diff --git a/src/routes/api/etapi_tokens.js b/src/routes/api/etapi_tokens.js
new file mode 100644
index 000000000..b6a2ebfa7
--- /dev/null
+++ b/src/routes/api/etapi_tokens.js
@@ -0,0 +1,30 @@
+const etapiTokenService = require("../../services/etapi_tokens");
+
+function getTokens() {
+ const tokens = etapiTokenService.getTokens();
+
+ tokens.sort((a, b) => a.utcDateCreated < b.utcDateCreated ? -1 : 1);
+
+ return tokens;
+}
+
+function createToken(req) {
+ return {
+ authToken: etapiTokenService.createToken(req.body.tokenName)
+ };
+}
+
+function patchToken(req) {
+ etapiTokenService.renameToken(req.params.etapiTokenId, req.body.name);
+}
+
+function deleteToken(req) {
+ etapiTokenService.deleteToken(req.params.etapiTokenId);
+}
+
+module.exports = {
+ getTokens,
+ createToken,
+ patchToken,
+ deleteToken
+};
\ No newline at end of file
diff --git a/src/routes/api/login.js b/src/routes/api/login.js
index 9e4cc318a..fa8685d8a 100644
--- a/src/routes/api/login.js
+++ b/src/routes/api/login.js
@@ -3,16 +3,15 @@
const options = require('../../services/options');
const utils = require('../../services/utils');
const dateUtils = require('../../services/date_utils');
-const sourceIdService = require('../../services/source_id');
+const instanceId = require('../../services/member_id');
const passwordEncryptionService = require('../../services/password_encryption');
const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info');
const eventService = require('../../services/events');
const sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql');
-const optionService = require('../../services/options');
-const ApiToken = require('../../becca/entities/api_token');
const ws = require("../../services/ws");
+const etapiTokenService = require("../../services/etapi_tokens");
function loginSync(req) {
if (!sqlInit.schemaExists()) {
@@ -48,7 +47,7 @@ function loginSync(req) {
req.session.loggedIn = true;
return {
- sourceId: sourceIdService.getCurrentSourceId(),
+ instanceId: instanceId,
maxEntityChangeId: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1")
};
}
@@ -85,23 +84,18 @@ function logoutFromProtectedSession() {
}
function token(req) {
- const username = req.body.username;
const password = req.body.password;
- const isUsernameValid = username === optionService.getOption('username');
- const isPasswordValid = passwordEncryptionService.verifyPassword(password);
-
- if (!isUsernameValid || !isPasswordValid) {
- return [401, "Incorrect username/password"];
+ if (!passwordEncryptionService.verifyPassword(password)) {
+ return [401, "Incorrect password"];
}
- const apiToken = new ApiToken({
- token: utils.randomSecureToken()
- }).save();
+ // for backwards compatibility with Sender which does not send the name
+ const tokenName = req.body.tokenName || "Trilium Sender / Web Clipper";
+
+ const {authToken} = etapiTokenService.createToken(tokenName);
- return {
- token: apiToken.token
- };
+ return { token: authToken };
}
module.exports = {
diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js
index 6b43bec7f..3a62749a7 100644
--- a/src/routes/api/notes.js
+++ b/src/routes/api/notes.js
@@ -73,9 +73,7 @@ function deleteNote(req) {
const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
- for (const branch of note.getParentBranches()) {
- noteService.deleteBranch(branch, deleteId, taskContext);
- }
+ noteService.deleteNote(note, deleteId, taskContext);
if (eraseNotes) {
noteService.eraseNotesWithDeleteId(deleteId);
diff --git a/src/routes/api/options.js b/src/routes/api/options.js
index 57c1549de..3df70f68d 100644
--- a/src/routes/api/options.js
+++ b/src/routes/api/options.js
@@ -6,7 +6,6 @@ const searchService = require('../../services/search/services/search');
// options allowed to be updated directly in options dialog
const ALLOWED_OPTIONS = new Set([
- 'username', // not exposed for update (not harmful anyway), needed for reading
'eraseEntitiesAfterTimeInSeconds',
'protectedSessionTimeout',
'noteRevisionSnapshotTimeInterval',
@@ -69,6 +68,8 @@ function getOptions() {
}
}
+ resultMap['isPasswordSet'] = !!optionMap['passwordVerificationHash'] ? 'true' : 'false';
+
return resultMap;
}
diff --git a/src/routes/api/password.js b/src/routes/api/password.js
index 3478c457f..47466bd7e 100644
--- a/src/routes/api/password.js
+++ b/src/routes/api/password.js
@@ -1,11 +1,26 @@
"use strict";
-const changePasswordService = require('../../services/change_password');
+const passwordService = require('../../services/password');
function changePassword(req) {
- return changePasswordService.changePassword(req.body.current_password, req.body.new_password);
+ if (passwordService.isPasswordSet()) {
+ return passwordService.changePassword(req.body.current_password, req.body.new_password);
+ }
+ else {
+ return passwordService.setPassword(req.body.new_password);
+ }
+}
+
+function resetPassword(req) {
+ // protection against accidental call (not a security measure)
+ if (req.query.really !== "yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes") {
+ return [400, "Incorrect password reset confirmation"];
+ }
+
+ return passwordService.resetPassword();
}
module.exports = {
- changePassword
+ changePassword,
+ resetPassword
};
diff --git a/src/routes/api/sender.js b/src/routes/api/sender.js
index d3c6a8a07..4e2773f37 100644
--- a/src/routes/api/sender.js
+++ b/src/routes/api/sender.js
@@ -15,7 +15,7 @@ function uploadImage(req) {
const originalName = "Sender image." + imageType(file.buffer).ext;
- const parentNote = dateNoteService.getDateNote(req.headers['x-local-date']);
+ const parentNote = dateNoteService.getDayNote(req.headers['x-local-date']);
const {note, noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
@@ -35,7 +35,7 @@ function uploadImage(req) {
}
function saveNote(req) {
- const parentNote = dateNoteService.getDateNote(req.headers['x-local-date']);
+ const parentNote = dateNoteService.getDayNote(req.headers['x-local-date']);
const {note, branch} = noteService.createNewNote({
parentNoteId: parentNote.noteId,
diff --git a/src/routes/api/setup.js b/src/routes/api/setup.js
index d2ec74b2e..dbaf44807 100644
--- a/src/routes/api/setup.js
+++ b/src/routes/api/setup.js
@@ -13,16 +13,14 @@ function getStatus() {
};
}
-async function setupNewDocument(req) {
- const { username, password, theme } = req.body;
-
- await sqlInit.createInitialDatabase(username, password, theme);
+async function setupNewDocument() {
+ await sqlInit.createInitialDatabase();
}
function setupSyncFromServer(req) {
- const { syncServerHost, syncProxy, username, password } = req.body;
+ const { syncServerHost, syncProxy, password } = req.body;
- return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, username, password);
+ return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, password);
}
function saveSyncSeed(req) {
diff --git a/src/routes/api/special_notes.js b/src/routes/api/special_notes.js
index 1814cf849..04ae3e55a 100644
--- a/src/routes/api/special_notes.js
+++ b/src/routes/api/special_notes.js
@@ -10,8 +10,8 @@ function getInboxNote(req) {
return specialNotesService.getInboxNote(req.params.date);
}
-function getDateNote(req) {
- return dateNoteService.getDateNote(req.params.date);
+function getDayNote(req) {
+ return dateNoteService.getDayNote(req.params.date);
}
function getWeekNote(req) {
@@ -26,7 +26,7 @@ function getYearNote(req) {
return dateNoteService.getYearNote(req.params.year);
}
-function getDateNotesForMonth(req) {
+function getDayNotesForMonth(req) {
const month = req.params.month;
return sql.getMap(`
@@ -68,11 +68,11 @@ function getHoistedNote() {
module.exports = {
getInboxNote,
- getDateNote,
+ getDayNote,
getWeekNote,
getMonthNote,
getYearNote,
- getDateNotesForMonth,
+ getDayNotesForMonth,
createSqlConsole,
saveSqlConsole,
createSearchNote,
diff --git a/src/routes/api/sync.js b/src/routes/api/sync.js
index a928540d2..3276e02a5 100644
--- a/src/routes/api/sync.js
+++ b/src/routes/api/sync.js
@@ -123,13 +123,45 @@ function forceNoteSync(req) {
function getChanged(req) {
const startTime = Date.now();
- const lastEntityChangeId = parseInt(req.query.lastEntityChangeId);
+ let lastEntityChangeId = parseInt(req.query.lastEntityChangeId);
+ const clientinstanceId = req.query.instanceId;
+ let filteredEntityChanges = [];
- const entityChanges = sql.getRows("SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000", [lastEntityChangeId]);
+ while (filteredEntityChanges.length === 0) {
+ const entityChanges = sql.getRows(`
+ SELECT *
+ FROM entity_changes
+ WHERE isSynced = 1
+ AND id > ?
+ ORDER BY id
+ LIMIT 1000`, [lastEntityChangeId]);
+
+ if (entityChanges.length === 0) {
+ break;
+ }
+
+ filteredEntityChanges = entityChanges.filter(ec => ec.instanceId !== clientinstanceId);
+
+ if (filteredEntityChanges.length === 0) {
+ lastEntityChangeId = entityChanges[entityChanges.length - 1].id;
+ }
+ }
+
+ const entityChangeRecords = syncService.getEntityChangeRecords(filteredEntityChanges);
+
+ if (entityChangeRecords.length > 0) {
+ lastEntityChangeId = entityChangeRecords[entityChangeRecords.length - 1].entityChange.id;
+ }
const ret = {
- entityChanges: syncService.getEntityChangesRecords(entityChanges),
- maxEntityChangeId: sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1')
+ entityChanges: entityChangeRecords,
+ lastEntityChangeId,
+ outstandingPullCount: sql.getValue(`
+ SELECT COUNT(id)
+ FROM entity_changes
+ WHERE isSynced = 1
+ AND instanceId != ?
+ AND id > ?`, [clientinstanceId, lastEntityChangeId])
};
if (ret.entityChanges.length > 0) {
@@ -174,10 +206,10 @@ function update(req) {
}
}
- const {entities} = body;
+ const {entities, instanceId} = body;
for (const {entityChange, entity} of entities) {
- syncUpdateService.updateEntity(entityChange, entity);
+ syncUpdateService.updateEntity(entityChange, entity, instanceId);
}
}
diff --git a/src/routes/index.js b/src/routes/index.js
index 6a1abe66a..04fe8cb02 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -1,6 +1,5 @@
"use strict";
-const sourceIdService = require('../services/source_id');
const sql = require('../services/sql');
const attributeService = require('../services/attributes');
const config = require('../services/config');
@@ -28,7 +27,6 @@ function index(req, res) {
mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize),
- sourceId: sourceIdService.generateSourceId(),
maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"),
maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"),
instanceName: config.General ? config.General.instanceName : null,
diff --git a/src/routes/login.js b/src/routes/login.js
index b5837059a..3431f3431 100644
--- a/src/routes/login.js
+++ b/src/routes/login.js
@@ -4,17 +4,47 @@ const utils = require('../services/utils');
const optionService = require('../services/options');
const myScryptService = require('../services/my_scrypt');
const log = require('../services/log');
+const passwordService = require("../services/password");
function loginPage(req, res) {
res.render('login', { failedAuth: false });
}
-function login(req, res) {
- const userName = optionService.getOption('username');
+function setPasswordPage(req, res) {
+ res.render('set_password', { error: false });
+}
+function setPassword(req, res) {
+ if (passwordService.isPasswordSet()) {
+ return [400, "Password has been already set"];
+ }
+
+ let {password1, password2} = req.body;
+ password1 = password1.trim();
+ password2 = password2.trim();
+
+ let error;
+
+ if (password1 !== password2) {
+ error = "Entered passwords don't match.";
+ } else if (password1.length < 4) {
+ error = "Password must be at least 4 characters long.";
+ }
+
+ if (error) {
+ res.render('set_password', { error });
+ return;
+ }
+
+ passwordService.setPassword(password1);
+
+ res.redirect('login');
+}
+
+function login(req, res) {
const guessedPassword = req.body.password;
- if (req.body.username === userName && verifyPassword(guessedPassword)) {
+ if (verifyPassword(guessedPassword)) {
const rememberMe = req.body.remember_me;
req.session.regenerate(() => {
@@ -30,7 +60,7 @@ function login(req, res) {
}
else {
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy
- log.info(`WARNING: Wrong username / password from ${req.ip}, rejecting.`);
+ log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
res.render('login', {'failedAuth': true});
}
@@ -55,6 +85,8 @@ function logout(req, res) {
module.exports = {
loginPage,
+ setPasswordPage,
+ setPassword,
login,
logout
};
diff --git a/src/routes/routes.js b/src/routes/routes.js
index 1b65257c4..31a84de8d 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -31,15 +31,22 @@ const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender');
const filesRoute = require('./api/files');
const searchRoute = require('./api/search');
-const specialNotesRoute = require('./api/special_notes.js');
-const noteMapRoute = require('./api/note_map.js');
+const specialNotesRoute = require('./api/special_notes');
+const noteMapRoute = require('./api/note_map');
const clipperRoute = require('./api/clipper');
const similarNotesRoute = require('./api/similar_notes');
const keysRoute = require('./api/keys');
const backendLogRoute = require('./api/backend_log');
const statsRoute = require('./api/stats');
const fontsRoute = require('./api/fonts');
+const etapiTokensApiRoutes = require('./api/etapi_tokens');
const shareRoutes = require('../share/routes');
+const etapiAuthRoutes = require('../etapi/auth');
+const etapiAttributeRoutes = require('../etapi/attributes');
+const etapiBranchRoutes = require('../etapi/branches');
+const etapiNoteRoutes = require('../etapi/notes');
+const etapiSpecialNoteRoutes = require('../etapi/special_notes');
+const etapiSpecRoute = require('../etapi/spec');
const log = require('../services/log');
const express = require('express');
@@ -51,7 +58,7 @@ const entityChangesService = require('../services/entity_changes');
const csurf = require('csurf');
const {createPartialContentHandler} = require("express-partial-content");
const rateLimit = require("express-rate-limit");
-const AbstractEntity = require("../becca/entities/abstract_entity.js");
+const AbstractEntity = require("../becca/entities/abstract_entity");
const csrfMiddleware = csurf({
cookie: true,
@@ -139,7 +146,7 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
cls.namespace.bindEmitter(res);
const result = cls.init(() => {
- cls.set('sourceId', req.headers['trilium-source-id']);
+ cls.set('componentId', req.headers['trilium-component-id']);
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root');
@@ -177,12 +184,13 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
});
}
-const GET = 'get', POST = 'post', PUT = 'put', DELETE = 'delete';
+const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete';
const uploadMiddleware = multer.single('upload');
function register(app) {
route(GET, '/', [auth.checkAuth, csrfMiddleware], indexRoute.index);
- route(GET, '/login', [auth.checkAppInitialized], loginRoute.loginPage);
+ route(GET, '/login', [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage);
+ route(GET, '/set-password', [auth.checkAppInitialized], loginRoute.setPasswordPage);
const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
@@ -191,6 +199,7 @@ function register(app) {
route(POST, '/login', [loginRateLimiter], loginRoute.login);
route(POST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout);
+ route(POST, '/set-password', [auth.checkAppInitialized], loginRoute.setPassword);
route(GET, '/setup', [], setupRoute.setupPage);
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
@@ -265,11 +274,11 @@ function register(app) {
apiRoute(GET, '/api/note-map/:noteId/backlinks', noteMapRoute.getBacklinks);
apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote);
- apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote);
- apiRoute(GET, '/api/special-notes/week/:date', specialNotesRoute.getWeekNote);
- apiRoute(GET, '/api/special-notes/month/:month', specialNotesRoute.getMonthNote);
- apiRoute(GET, '/api/special-notes/year/:year', specialNotesRoute.getYearNote);
- apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDateNotesForMonth);
+ apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote);
+ apiRoute(GET, '/api/special-notes/weeks/:date', specialNotesRoute.getWeekNote);
+ apiRoute(GET, '/api/special-notes/months/:month', specialNotesRoute.getMonthNote);
+ apiRoute(GET, '/api/special-notes/years/:year', specialNotesRoute.getYearNote);
+ apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDayNotesForMonth);
apiRoute(POST, '/api/special-notes/sql-console', specialNotesRoute.createSqlConsole);
apiRoute(POST, '/api/special-notes/save-sql-console', specialNotesRoute.saveSqlConsole);
apiRoute(POST, '/api/special-notes/search-note', specialNotesRoute.createSearchNote);
@@ -288,6 +297,7 @@ function register(app) {
apiRoute(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes);
apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword);
+ apiRoute(POST, '/api/password/reset', passwordApiRoute.resetPassword);
apiRoute(POST, '/api/sync/test', syncApiRoute.testSync);
apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow);
@@ -333,8 +343,8 @@ function register(app) {
// no CSRF since this is called from android app
route(POST, '/api/sender/login', [], loginApiRoute.token, apiResultHandler);
- route(POST, '/api/sender/image', [auth.checkToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
- route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler);
+ route(POST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
+ route(POST, '/api/sender/note', [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler);
apiRoute(GET, '/api/quick-search/:searchString', searchRoute.quickSearch);
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
@@ -350,7 +360,7 @@ function register(app) {
route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler);
// in case of local electron, local calls are allowed unauthenticated, for server they need auth
- const clipperMiddleware = utils.isElectron() ? [] : [auth.checkToken];
+ const clipperMiddleware = utils.isElectron() ? [] : [auth.checkEtapiToken];
route(GET, '/api/clipper/handshake', clipperMiddleware, clipperRoute.handshake, apiResultHandler);
route(POST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler);
@@ -371,7 +381,19 @@ function register(app) {
route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
+ apiRoute(GET, '/api/etapi-tokens', etapiTokensApiRoutes.getTokens);
+ apiRoute(POST, '/api/etapi-tokens', etapiTokensApiRoutes.createToken);
+ apiRoute(PATCH, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.patchToken);
+ apiRoute(DELETE, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.deleteToken);
+
shareRoutes.register(router);
+
+ etapiAuthRoutes.register(router);
+ etapiAttributeRoutes.register(router);
+ etapiBranchRoutes.register(router);
+ etapiNoteRoutes.register(router);
+ etapiSpecialNoteRoutes.register(router);
+ etapiSpecRoute.register(router);
app.use('', router);
}
diff --git a/src/services/app_info.js b/src/services/app_info.js
index 29ef3a316..0cd8f8384 100644
--- a/src/services/app_info.js
+++ b/src/services/app_info.js
@@ -4,8 +4,8 @@ const build = require('./build');
const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
-const APP_DB_VERSION = 189;
-const SYNC_VERSION = 23;
+const APP_DB_VERSION = 194;
+const SYNC_VERSION = 25;
const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = {
diff --git a/src/services/auth.js b/src/services/auth.js
index 2e6c6bfe9..73c693219 100644
--- a/src/services/auth.js
+++ b/src/services/auth.js
@@ -1,12 +1,12 @@
"use strict";
-const sql = require('./sql');
+const etapiTokenService = require("./etapi_tokens");
const log = require('./log');
const sqlInit = require('./sql_init');
const utils = require('./utils');
const passwordEncryptionService = require('./password_encryption');
-const optionService = require('./options');
const config = require('./config');
+const passwordService = require("./password");
const noAuthentication = config.General && config.General.noAuthentication === true;
@@ -15,7 +15,11 @@ function checkAuth(req, res, next) {
res.redirect("setup");
}
else if (!req.session.loggedIn && !utils.isElectron() && !noAuthentication) {
- res.redirect("login");
+ if (passwordService.isPasswordSet()) {
+ res.redirect("login");
+ } else {
+ res.redirect("set-password");
+ }
}
else {
next();
@@ -51,6 +55,14 @@ function checkAppInitialized(req, res, next) {
}
}
+function checkPasswordSet(req, res, next) {
+ if (!utils.isElectron() && !passwordService.isPasswordSet()) {
+ res.redirect("set-password");
+ } else {
+ next();
+ }
+}
+
function checkAppNotInitialized(req, res, next) {
if (sqlInit.isDbInitialized()) {
reject(req, res, "App already initialized.");
@@ -60,15 +72,12 @@ function checkAppNotInitialized(req, res, next) {
}
}
-function checkToken(req, res, next) {
- const token = req.headers.authorization;
-
- // TODO: put all tokens into becca memory to avoid these requests
- if (sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
- reject(req, res, "Token not found");
+function checkEtapiToken(req, res, next) {
+ if (etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
+ next();
}
else {
- next();
+ reject(req, res, "Token not found");
}
}
@@ -87,10 +96,10 @@ function checkCredentials(req, res, next) {
const auth = new Buffer.from(header, 'base64').toString();
const [username, password] = auth.split(/:/);
- const dbUsername = optionService.getOption('username');
+ // username is ignored
- if (dbUsername !== username || !passwordEncryptionService.verifyPassword(password)) {
- res.status(401).send('Incorrect username and/or password');
+ if (!passwordEncryptionService.verifyPassword(password)) {
+ res.status(401).send('Incorrect password');
}
else {
next();
@@ -101,8 +110,9 @@ module.exports = {
checkAuth,
checkApiAuth,
checkAppInitialized,
+ checkPasswordSet,
checkAppNotInitialized,
checkApiAuthOrElectron,
- checkToken,
+ checkEtapiToken,
checkCredentials
};
diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js
index d6d856725..89f90a7d2 100644
--- a/src/services/backend_script_api.js
+++ b/src/services/backend_script_api.js
@@ -134,18 +134,18 @@ function BackendScriptApi(currentNote, apiParams) {
this.getNoteWithLabel = attributeService.getNoteWithLabel;
/**
- * If there's no branch between note and parent note, create one. Otherwise do nothing.
+ * If there's no branch between note and parent note, create one. Otherwise, do nothing.
*
* @method
* @param {string} noteId
* @param {string} parentNoteId
- * @param {string} prefix - if branch will be create between note and parent note, set this prefix
+ * @param {string} prefix - if branch will be created between note and parent note, set this prefix
* @returns {void}
*/
this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent;
/**
- * If there's a branch between note and parent note, remove it. Otherwise do nothing.
+ * If there's a branch between note and parent note, remove it. Otherwise, do nothing.
*
* @method
* @param {string} noteId
@@ -309,8 +309,18 @@ function BackendScriptApi(currentNote, apiParams) {
* @method
* @param {string} date in YYYY-MM-DD format
* @returns {Note|null}
+ * @deprecated use getDayNote instead
*/
- this.getDateNote = dateNoteService.getDateNote;
+ this.getDateNote = dateNoteService.getDayNote;
+
+ /**
+ * Returns day note for given date. If such note doesn't exist, it is created.
+ *
+ * @method
+ * @param {string} date in YYYY-MM-DD format
+ * @returns {Note|null}
+ */
+ this.getDayNote = dateNoteService.getDayNote;
/**
* Returns today's day note. If such note doesn't exist, it is created.
diff --git a/src/services/backup.js b/src/services/backup.js
index f43ead594..948a1d470 100644
--- a/src/services/backup.js
+++ b/src/services/backup.js
@@ -76,7 +76,7 @@ async function anonymize() {
const db = new Database(anonymizedFile);
- db.prepare("UPDATE api_tokens SET token = 'API token value'").run();
+ db.prepare("UPDATE etapi_tokens SET tokenHash = 'API token hash value'").run();
db.prepare("UPDATE notes SET title = 'title'").run();
db.prepare("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL").run();
db.prepare("UPDATE note_revisions SET title = 'title'").run();
diff --git a/src/services/change_password.js b/src/services/change_password.js
deleted file mode 100644
index b06e71328..000000000
--- a/src/services/change_password.js
+++ /dev/null
@@ -1,37 +0,0 @@
-"use strict";
-
-const sql = require('./sql');
-const optionService = require('./options');
-const myScryptService = require('./my_scrypt');
-const utils = require('./utils');
-const passwordEncryptionService = require('./password_encryption');
-
-function changePassword(currentPassword, newPassword) {
- if (!passwordEncryptionService.verifyPassword(currentPassword)) {
- return {
- success: false,
- message: "Given current password doesn't match hash"
- };
- }
-
- sql.transactional(() => {
- const decryptedDataKey = passwordEncryptionService.getDataKey(currentPassword);
-
- optionService.setOption('passwordVerificationSalt', utils.randomSecureToken(32));
- optionService.setOption('passwordDerivedKeySalt', utils.randomSecureToken(32));
-
- const newPasswordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(newPassword));
-
- passwordEncryptionService.setDataKey(newPassword, decryptedDataKey);
-
- optionService.setOption('passwordVerificationHash', newPasswordVerificationKey);
- });
-
- return {
- success: true
- };
-}
-
-module.exports = {
- changePassword
-};
diff --git a/src/services/cls.js b/src/services/cls.js
index 7f594fdda..29ea06778 100644
--- a/src/services/cls.js
+++ b/src/services/cls.js
@@ -28,8 +28,8 @@ function getHoistedNoteId() {
return namespace.get('hoistedNoteId') || 'root';
}
-function getSourceId() {
- return namespace.get('sourceId');
+function getComponentId() {
+ return namespace.get('componentId');
}
function getLocalNowDateTime() {
@@ -80,7 +80,7 @@ module.exports = {
set,
namespace,
getHoistedNoteId,
- getSourceId,
+ getComponentId,
getLocalNowDateTime,
disableEntityEvents,
isEntityEventsDisabled,
diff --git a/src/services/consistency_checks.js b/src/services/consistency_checks.js
index 6198d9bec..b7ba1fff6 100644
--- a/src/services/consistency_checks.js
+++ b/src/services/consistency_checks.js
@@ -567,7 +567,7 @@ class ConsistencyChecks {
this.runEntityChangeChecks("note_revisions", "noteRevisionId");
this.runEntityChangeChecks("branches", "branchId");
this.runEntityChangeChecks("attributes", "attributeId");
- this.runEntityChangeChecks("api_tokens", "apiTokenId");
+ this.runEntityChangeChecks("etapi_tokens", "etapiTokenId");
this.runEntityChangeChecks("options", "name");
}
@@ -660,7 +660,7 @@ class ConsistencyChecks {
return `${tableName}: ${count}`;
}
- const tables = [ "notes", "note_revisions", "branches", "attributes", "api_tokens" ];
+ const tables = [ "notes", "note_revisions", "branches", "attributes", "etapi_tokens" ];
log.info("Table counts: " + tables.map(tableName => getTableRowCount(tableName)).join(", "));
}
diff --git a/src/services/content_hash.js b/src/services/content_hash.js
index 33f2e730c..787db3be5 100644
--- a/src/services/content_hash.js
+++ b/src/services/content_hash.js
@@ -8,9 +8,9 @@ function getEntityHashes() {
const startTime = new Date();
const hashRows = sql.getRawRows(`
- SELECT entityName,
- entityId,
- hash
+ SELECT entityName,
+ entityId,
+ hash
FROM entity_changes
WHERE isSynced = 1
AND entityName != 'note_reordering'`);
diff --git a/src/services/date_notes.js b/src/services/date_notes.js
index 254032e1b..b595791c0 100644
--- a/src/services/date_notes.js
+++ b/src/services/date_notes.js
@@ -3,7 +3,6 @@
const noteService = require('./notes');
const attributeService = require('./attributes');
const dateUtils = require('./date_utils');
-const becca = require('../becca/becca');
const sql = require('./sql');
const protectedSessionService = require('./protected_session');
@@ -49,7 +48,7 @@ function getRootCalendarNote() {
}
/** @returns {Note} */
-function getYearNote(dateStr, rootNote) {
+function getYearNote(dateStr, rootNote = null) {
if (!rootNote) {
rootNote = getRootCalendarNote();
}
@@ -88,7 +87,7 @@ function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
}
/** @returns {Note} */
-function getMonthNote(dateStr, rootNote) {
+function getMonthNote(dateStr, rootNote = null) {
if (!rootNote) {
rootNote = getRootCalendarNote();
}
@@ -124,7 +123,7 @@ function getMonthNote(dateStr, rootNote) {
return monthNote;
}
-function getDateNoteTitle(rootNote, dayNumber, dateObj) {
+function getDayNoteTitle(rootNote, dayNumber, dateObj) {
const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}";
const weekDay = DAYS[dateObj.getDay()];
@@ -137,7 +136,7 @@ function getDateNoteTitle(rootNote, dayNumber, dateObj) {
}
/** @returns {Note} */
-function getDateNote(dateStr) {
+function getDayNote(dateStr) {
dateStr = dateStr.trim().substr(0, 10);
let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
@@ -152,7 +151,7 @@ function getDateNote(dateStr) {
const dateObj = dateUtils.parseLocalDate(dateStr);
- const noteTitle = getDateNoteTitle(rootNote, dayNumber, dateObj);
+ const noteTitle = getDayNoteTitle(rootNote, dayNumber, dateObj);
sql.transactional(() => {
dateNote = createNote(monthNote, noteTitle);
@@ -170,7 +169,7 @@ function getDateNote(dateStr) {
}
function getTodayNote() {
- return getDateNote(dateUtils.localNowDate());
+ return getDayNote(dateUtils.localNowDate());
}
function getStartOfTheWeek(date, startOfTheWeek) {
@@ -197,7 +196,7 @@ function getWeekNote(dateStr, options = {}) {
dateStr = dateUtils.utcDateTimeStr(dateObj);
- return getDateNote(dateStr);
+ return getDayNote(dateStr);
}
module.exports = {
@@ -205,6 +204,6 @@ module.exports = {
getYearNote,
getMonthNote,
getWeekNote,
- getDateNote,
+ getDayNote,
getTodayNote
};
diff --git a/src/services/entity_changes.js b/src/services/entity_changes.js
index b02b4c538..7e040b269 100644
--- a/src/services/entity_changes.js
+++ b/src/services/entity_changes.js
@@ -1,13 +1,19 @@
const sql = require('./sql');
-const sourceIdService = require('./source_id');
const dateUtils = require('./date_utils');
const log = require('./log');
const cls = require('./cls');
const utils = require('./utils');
+const instanceId = require('./member_id');
const becca = require("../becca/becca");
let maxEntityChangeId = 0;
+function addEntityChangeWithinstanceId(origEntityChange, instanceId) {
+ const ec = {...origEntityChange, instanceId};
+
+ return addEntityChange(ec);
+}
+
function addEntityChange(origEntityChange) {
const ec = {...origEntityChange};
@@ -17,7 +23,8 @@ function addEntityChange(origEntityChange) {
ec.changeId = utils.randomString(12);
}
- ec.sourceId = ec.sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId();
+ ec.componentId = ec.componentId || cls.getComponentId() || "";
+ ec.instanceId = ec.instanceId || instanceId;
ec.isSynced = ec.isSynced ? 1 : 0;
ec.isErased = ec.isErased ? 1 : 0;
ec.id = sql.replace("entity_changes", ec);
@@ -27,7 +34,7 @@ function addEntityChange(origEntityChange) {
cls.addEntityChange(ec);
}
-function addNoteReorderingEntityChange(parentNoteId, sourceId) {
+function addNoteReorderingEntityChange(parentNoteId, componentId) {
addEntityChange({
entityName: "note_reordering",
entityId: parentNoteId,
@@ -35,7 +42,8 @@ function addNoteReorderingEntityChange(parentNoteId, sourceId) {
isErased: false,
utcDateChanged: dateUtils.utcNowDateTime(),
isSynced: true,
- sourceId
+ componentId,
+ instanceId: instanceId
});
const eventService = require('./events');
@@ -129,7 +137,7 @@ function fillAllEntityChanges() {
fillEntityChanges("note_revision_contents", "noteRevisionId");
fillEntityChanges("recent_notes", "noteId");
fillEntityChanges("attributes", "attributeId");
- fillEntityChanges("api_tokens", "apiTokenId");
+ fillEntityChanges("etapi_tokens", "etapiTokenId");
fillEntityChanges("options", "name", 'isSynced = 1');
});
}
@@ -138,6 +146,7 @@ module.exports = {
addNoteReorderingEntityChange,
moveEntityChangeToTop,
addEntityChange,
+ addEntityChangeWithinstanceId,
fillAllEntityChanges,
addEntityChangesForSector,
getMaxEntityChangeId: () => maxEntityChangeId
diff --git a/src/services/etapi_tokens.js b/src/services/etapi_tokens.js
new file mode 100644
index 000000000..145e6d089
--- /dev/null
+++ b/src/services/etapi_tokens.js
@@ -0,0 +1,107 @@
+const becca = require("../becca/becca");
+const utils = require("./utils");
+const EtapiToken = require("../becca/entities/etapi_token");
+const crypto = require("crypto");
+
+function getTokens() {
+ return becca.getEtapiTokens();
+}
+
+function getTokenHash(token) {
+ return crypto.createHash('sha256').update(token).digest('base64');
+}
+
+function createToken(tokenName) {
+ const token = utils.randomSecureToken();
+ const tokenHash = getTokenHash(token);
+
+ const etapiToken = new EtapiToken({
+ name: tokenName,
+ tokenHash
+ }).save();
+
+ return {
+ authToken: `${etapiToken.etapiTokenId}_${token}`
+ };
+}
+
+function parseAuthToken(auth) {
+ if (!auth) {
+ return null;
+ }
+
+ const chunks = auth.split("_");
+
+ if (chunks.length === 1) {
+ return { token: auth }; // legacy format without etapiTokenId
+ }
+ else if (chunks.length === 2) {
+ return {
+ etapiTokenId: chunks[0],
+ token: chunks[1]
+ }
+ }
+ else {
+ return null; // wrong format
+ }
+}
+
+function isValidAuthHeader(auth) {
+ const parsed = parseAuthToken(auth);
+
+ if (!parsed) {
+ return false;
+ }
+
+ const authTokenHash = getTokenHash(parsed.token);
+
+ if (parsed.etapiTokenId) {
+ const etapiToken = becca.getEtapiToken(parsed.etapiTokenId);
+
+ if (!etapiToken) {
+ return false;
+ }
+
+ return etapiToken.tokenHash === authTokenHash;
+ }
+ else {
+ for (const etapiToken of becca.getEtapiTokens()) {
+ if (etapiToken.tokenHash === authTokenHash) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
+function renameToken(etapiTokenId, newName) {
+ const etapiToken = becca.getEtapiToken(etapiTokenId);
+
+ if (!etapiToken) {
+ throw new Error(`Token ${etapiTokenId} does not exist`);
+ }
+
+ etapiToken.name = newName;
+ etapiToken.save();
+}
+
+function deleteToken(etapiTokenId) {
+ const etapiToken = becca.getEtapiToken(etapiTokenId);
+
+ if (!etapiToken) {
+ return; // ok, already deleted
+ }
+
+ etapiToken.isDeleted = true;
+ etapiToken.save();
+}
+
+module.exports = {
+ getTokens,
+ createToken,
+ renameToken,
+ deleteToken,
+ parseAuthToken,
+ isValidAuthHeader
+};
\ No newline at end of file
diff --git a/src/services/member_id.js b/src/services/member_id.js
new file mode 100644
index 000000000..4ee187b01
--- /dev/null
+++ b/src/services/member_id.js
@@ -0,0 +1,5 @@
+const utils = require('./utils');
+
+const instanceId = utils.randomString(12);
+
+module.exports = instanceId;
diff --git a/src/services/notes.js b/src/services/notes.js
index c7cd68b3d..304b406dc 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -18,6 +18,8 @@ const Branch = require('../becca/entities/branch');
const Note = require('../becca/entities/note');
const Attribute = require('../becca/entities/attribute');
+// TODO: patch/put note content
+
function getNewNotePosition(parentNoteId) {
const note = becca.notes[parentNoteId];
@@ -107,6 +109,10 @@ function createNewNote(params) {
throw new Error(`Note title must be set`);
}
+ if (params.content === null || params.content === undefined) {
+ throw new Error(`Note content must be set`);
+ }
+
return sql.transactional(() => {
const note = new Note({
noteId: params.noteId, // optionally can force specific noteId
@@ -520,7 +526,7 @@ function updateNote(noteId, noteUpdates) {
/**
* @param {Branch} branch
- * @param {string} deleteId
+ * @param {string|null} deleteId
* @param {TaskContext} taskContext
*
* @return {boolean} - true if note has been deleted, false otherwise
@@ -570,6 +576,17 @@ function deleteBranch(branch, deleteId, taskContext) {
}
}
+/**
+ * @param {Note} note
+ * @param {string|null} deleteId
+ * @param {TaskContext} taskContext
+ */
+function deleteNote(note, deleteId, taskContext) {
+ for (const branch of note.getParentBranches()) {
+ deleteBranch(branch, deleteId, taskContext);
+ }
+}
+
/**
* @param {string} noteId
* @param {TaskContext} taskContext
@@ -915,6 +932,7 @@ module.exports = {
createNewNoteWithTarget,
updateNote,
deleteBranch,
+ deleteNote,
undeleteNote,
protectNoteRecursively,
scanForLinks,
diff --git a/src/services/options.js b/src/services/options.js
index da883ad27..b9abb526b 100644
--- a/src/services/options.js
+++ b/src/services/options.js
@@ -1,5 +1,5 @@
const becca = require('../becca/becca');
-const sql = require("./sql.js");
+const sql = require("./sql");
function getOption(name) {
let option;
diff --git a/src/services/options_init.js b/src/services/options_init.js
index 8ee380566..5744148c3 100644
--- a/src/services/options_init.js
+++ b/src/services/options_init.js
@@ -1,6 +1,4 @@
const optionService = require('./options');
-const passwordEncryptionService = require('./password_encryption');
-const myScryptService = require('./my_scrypt');
const appInfo = require('./app_info');
const utils = require('./utils');
const log = require('./log');
@@ -12,21 +10,6 @@ function initDocumentOptions() {
optionService.createOption('documentSecret', utils.randomSecureToken(16), false);
}
-function initSyncedOptions(username, password) {
- optionService.createOption('username', username, true);
-
- optionService.createOption('passwordVerificationSalt', utils.randomSecureToken(32), true);
- optionService.createOption('passwordDerivedKeySalt', utils.randomSecureToken(32), true);
-
- const passwordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(password), true);
- optionService.createOption('passwordVerificationHash', passwordVerificationKey, true);
-
- // passwordEncryptionService expects these options to already exist
- optionService.createOption('encryptedDataKey', '', true);
-
- passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16), true);
-}
-
function initNotSyncedOptions(initialized, opts = {}) {
optionService.createOption('openTabs', JSON.stringify([
{
@@ -45,7 +28,15 @@ function initNotSyncedOptions(initialized, opts = {}) {
optionService.createOption('lastSyncedPull', '0', false);
optionService.createOption('lastSyncedPush', '0', false);
- optionService.createOption('theme', opts.theme || 'white', false);
+ let theme = 'dark'; // default based on the poll in https://github.com/zadam/trilium/issues/2516
+
+ if (utils.isElectron()) {
+ const {nativeTheme} = require('electron');
+
+ theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
+ }
+
+ optionService.createOption('theme', theme, false);
optionService.createOption('syncServerHost', opts.syncServerHost || '', false);
optionService.createOption('syncServerTimeout', '120000', false);
@@ -130,7 +121,6 @@ function getKeyboardDefaultOptions() {
module.exports = {
initDocumentOptions,
- initSyncedOptions,
initNotSyncedOptions,
initStartupOptions
};
diff --git a/src/services/password.js b/src/services/password.js
new file mode 100644
index 000000000..f88de5062
--- /dev/null
+++ b/src/services/password.js
@@ -0,0 +1,83 @@
+"use strict";
+
+const sql = require('./sql');
+const optionService = require('./options');
+const myScryptService = require('./my_scrypt');
+const utils = require('./utils');
+const passwordEncryptionService = require('./password_encryption');
+
+function isPasswordSet() {
+ return !!sql.getValue("SELECT value FROM options WHERE name = 'passwordVerificationHash'");
+}
+
+function changePassword(currentPassword, newPassword) {
+ if (!isPasswordSet()) {
+ throw new Error("Password has not been set yet, so it cannot be changed. Use 'setPassword' instead.");
+ }
+
+ if (!passwordEncryptionService.verifyPassword(currentPassword)) {
+ return {
+ success: false,
+ message: "Given current password doesn't match hash"
+ };
+ }
+
+ sql.transactional(() => {
+ const decryptedDataKey = passwordEncryptionService.getDataKey(currentPassword);
+
+ optionService.setOption('passwordVerificationSalt', utils.randomSecureToken(32));
+ optionService.setOption('passwordDerivedKeySalt', utils.randomSecureToken(32));
+
+ const newPasswordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(newPassword));
+
+ passwordEncryptionService.setDataKey(newPassword, decryptedDataKey);
+
+ optionService.setOption('passwordVerificationHash', newPasswordVerificationKey);
+ });
+
+ return {
+ success: true
+ };
+}
+
+function setPassword(password) {
+ if (isPasswordSet()) {
+ throw new Error("Password is set already. Either change it or perform 'reset password' first.");
+ }
+
+ optionService.createOption('passwordVerificationSalt', utils.randomSecureToken(32), true);
+ optionService.createOption('passwordDerivedKeySalt', utils.randomSecureToken(32), true);
+
+ const passwordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(password), true);
+ optionService.createOption('passwordVerificationHash', passwordVerificationKey, true);
+
+ // passwordEncryptionService expects these options to already exist
+ optionService.createOption('encryptedDataKey', '', true);
+
+ passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16), true);
+
+ return {
+ success: true
+ };
+}
+
+function resetPassword() {
+ // user forgot the password,
+ sql.transactional(() => {
+ optionService.setOption('passwordVerificationSalt', '');
+ optionService.setOption('passwordDerivedKeySalt', '');
+ optionService.setOption('encryptedDataKey', '');
+ optionService.setOption('passwordVerificationHash', '');
+ });
+
+ return {
+ success: true
+ };
+}
+
+module.exports = {
+ isPasswordSet,
+ changePassword,
+ setPassword,
+ resetPassword
+};
diff --git a/src/services/request.js b/src/services/request.js
index 3033ab0f7..30e7d9e09 100644
--- a/src/services/request.js
+++ b/src/services/request.js
@@ -38,7 +38,7 @@ function exec(opts) {
};
if (opts.auth) {
- headers['trilium-cred'] = Buffer.from(opts.auth.username + ":" + opts.auth.password).toString('base64');
+ headers['trilium-cred'] = Buffer.from("dummy:" + opts.auth.password).toString('base64');
}
const request = client.request({
diff --git a/src/services/script.js b/src/services/script.js
index e59eceeaa..1079fe5a1 100644
--- a/src/services/script.js
+++ b/src/services/script.js
@@ -30,9 +30,9 @@ function executeBundle(bundle, apiParams = {}) {
apiParams.startNote = bundle.note;
}
- const originalSourceId = cls.get('sourceId');
+ const originalComponentId = cls.get('componentId');
- cls.set('sourceId', 'script');
+ cls.set('componentId', 'script');
// last \r\n is necessary if script contains line comment on its last line
const script = "function() {\r\n" + bundle.script + "\r\n}";
@@ -47,7 +47,7 @@ function executeBundle(bundle, apiParams = {}) {
throw e;
}
finally {
- cls.set('sourceId', originalSourceId);
+ cls.set('componentId', originalComponentId);
}
}
diff --git a/src/services/search/search_context.js b/src/services/search/search_context.js
index 965a2b10b..569f773b8 100644
--- a/src/services/search/search_context.js
+++ b/src/services/search/search_context.js
@@ -18,6 +18,7 @@ class SearchContext {
this.orderDirection = params.orderDirection;
this.limit = params.limit;
this.debug = params.debug;
+ this.debugInfo = null;
this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch;
this.highlightedTokens = [];
this.originalQuery = "";
diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js
index ac92a2c02..02d2cc3bb 100644
--- a/src/services/search/services/parse.js
+++ b/src/services/search/services/parse.js
@@ -11,7 +11,7 @@ const RelationWhereExp = require('../expressions/relation_where');
const PropertyComparisonExp = require('../expressions/property_comparison');
const AttributeExistsExp = require('../expressions/attribute_exists');
const LabelComparisonExp = require('../expressions/label_comparison');
-const NoteFlatTextExp = require('../expressions/note_flat_text.js');
+const NoteFlatTextExp = require('../expressions/note_flat_text');
const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext');
const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit');
diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js
index 37766ca15..c8729e365 100644
--- a/src/services/search/services/search.js
+++ b/src/services/search/services/search.js
@@ -124,9 +124,13 @@ function parseQueryToExpression(query, searchContext) {
});
if (searchContext.debug) {
- log.info(`Fulltext tokens: ` + JSON.stringify(fulltextTokens));
- log.info(`Expression tokens: ` + JSON.stringify(structuredExpressionTokens, null, 4));
- log.info("Expression tree: " + JSON.stringify(expression, null, 4));
+ searchContext.debugInfo = {
+ fulltextTokens,
+ structuredExpressionTokens,
+ expression
+ };
+
+ log.info("Search debug: " + JSON.stringify(searchContext.debugInfo, null, 4));
}
return expression;
diff --git a/src/services/setup.js b/src/services/setup.js
index 271f8ce3c..eb176bebe 100644
--- a/src/services/setup.js
+++ b/src/services/setup.js
@@ -55,7 +55,7 @@ async function requestToSyncServer(method, path, body = null) {
}), timeout);
}
-async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) {
+async function setupSyncFromSyncServer(syncServerHost, syncProxy, password) {
if (sqlInit.isDbInitialized()) {
return {
result: 'failure',
@@ -70,10 +70,7 @@ async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, pass
const resp = await request.exec({
method: 'get',
url: syncServerHost + '/api/setup/sync-seed',
- auth: {
- username,
- password
- },
+ auth: { password },
proxy: syncProxy,
timeout: 30000 // seed request should not take long
});
diff --git a/src/services/source_id.js b/src/services/source_id.js
deleted file mode 100644
index f5917da8e..000000000
--- a/src/services/source_id.js
+++ /dev/null
@@ -1,27 +0,0 @@
-const utils = require('./utils');
-
-const localSourceIds = {};
-
-function generateSourceId() {
- const sourceId = utils.randomString(12);
-
- localSourceIds[sourceId] = true;
-
- return sourceId;
-}
-
-function isLocalSourceId(srcId) {
- return !!localSourceIds[srcId];
-}
-
-const currentSourceId = generateSourceId();
-
-function getCurrentSourceId() {
- return currentSourceId;
-}
-
-module.exports = {
- generateSourceId,
- getCurrentSourceId,
- isLocalSourceId
-};
diff --git a/src/services/special_notes.js b/src/services/special_notes.js
index 2c572072d..91dfc8ff8 100644
--- a/src/services/special_notes.js
+++ b/src/services/special_notes.js
@@ -23,7 +23,7 @@ function getInboxNote(date) {
}
else {
inbox = attributeService.getNoteWithLabel('inbox')
- || dateNoteService.getDateNote(date);
+ || dateNoteService.getDayNote(date);
}
return inbox;
@@ -137,7 +137,7 @@ function saveSqlConsole(sqlConsoleNoteId) {
const sqlConsoleHome =
attributeService.getNoteWithLabel('sqlConsoleHome')
- || dateNoteService.getDateNote(today);
+ || dateNoteService.getDayNote(today);
const result = sqlConsoleNote.cloneTo(sqlConsoleHome.noteId);
@@ -179,7 +179,7 @@ function getSearchHome() {
const today = dateUtils.localNowDate();
return hoistedNote.searchNoteInSubtree('#searchHome')
- || dateNoteService.getDateNote(today);
+ || dateNoteService.getDayNote(today);
}
}
diff --git a/src/services/sql.js b/src/services/sql.js
index 76ebb52ef..7b5ed8bc0 100644
--- a/src/services/sql.js
+++ b/src/services/sql.js
@@ -14,6 +14,8 @@ const cls = require('./cls');
const dbConnection = new Database(dataDir.DOCUMENT_PATH);
dbConnection.pragma('journal_mode = WAL');
+const LOG_ALL_QUERIES = false;
+
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
process.on(eventType, () => {
if (dbConnection) {
@@ -135,6 +137,10 @@ function getRawRows(query, params = []) {
}
function iterateRows(query, params = []) {
+ if (LOG_ALL_QUERIES) {
+ console.log(query);
+ }
+
return stmt(query).iterate(params);
}
@@ -157,11 +163,11 @@ function execute(query, params = []) {
return wrap(query, s => s.run(params));
}
-function executeWithoutTransaction(query, params = []) {
- dbConnection.run(query, params);
-}
-
function executeMany(query, params) {
+ if (LOG_ALL_QUERIES) {
+ console.log(query);
+ }
+
while (params.length > 0) {
const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
params = params.slice(curParams.length);
@@ -182,6 +188,10 @@ function executeMany(query, params) {
}
function executeScript(query) {
+ if (LOG_ALL_QUERIES) {
+ console.log(query);
+ }
+
return dbConnection.exec(query);
}
@@ -189,6 +199,10 @@ function wrap(query, func) {
const startTimestamp = Date.now();
let result;
+ if (LOG_ALL_QUERIES) {
+ console.log(query);
+ }
+
try {
result = func(stmt(query));
}
@@ -331,7 +345,6 @@ module.exports = {
* @param {object[]} [params] - array of params if needed
*/
execute,
- executeWithoutTransaction,
executeMany,
executeScript,
transactional,
diff --git a/src/services/sql_init.js b/src/services/sql_init.js
index b712e35a9..ae71390d4 100644
--- a/src/services/sql_init.js
+++ b/src/services/sql_init.js
@@ -45,9 +45,7 @@ async function initDbConnection() {
dbReady.resolve();
}
-async function createInitialDatabase(username, password, theme) {
- log.info("Creating database schema ...");
-
+async function createInitialDatabase() {
if (isDbInitialized()) {
throw new Error("DB is already initialized");
}
@@ -57,9 +55,9 @@ async function createInitialDatabase(username, password, theme) {
let rootNote;
- log.info("Creating root note ...");
-
sql.transactional(() => {
+ log.info("Creating database schema ...");
+
sql.executeScript(schema);
require("../becca/becca_loader").load();
@@ -67,6 +65,8 @@ async function createInitialDatabase(username, password, theme) {
const Note = require("../becca/entities/note");
const Branch = require("../becca/entities/branch");
+ log.info("Creating root note ...");
+
rootNote = new Note({
noteId: 'root',
title: 'root',
@@ -87,9 +87,9 @@ async function createInitialDatabase(username, password, theme) {
const optionsInitService = require('./options_init');
optionsInitService.initDocumentOptions();
- optionsInitService.initSyncedOptions(username, password);
- optionsInitService.initNotSyncedOptions(true, { theme });
+ optionsInitService.initNotSyncedOptions(true, {});
optionsInitService.initStartupOptions();
+ require("./password").resetPassword();
});
log.info("Importing demo content ...");
@@ -170,7 +170,6 @@ module.exports = {
dbReady,
schemaExists,
isDbInitialized,
- initDbConnection,
createInitialDatabase,
createDatabaseForSync,
setDbAsInitialized
diff --git a/src/services/sync.js b/src/services/sync.js
index d764d1ba8..dffd5d76f 100644
--- a/src/services/sync.js
+++ b/src/services/sync.js
@@ -4,7 +4,7 @@ const log = require('./log');
const sql = require('./sql');
const optionService = require('./options');
const utils = require('./utils');
-const sourceIdService = require('./source_id');
+const instanceId = require('./member_id');
const dateUtils = require('./date_utils');
const syncUpdateService = require('./sync_update');
const contentHashService = require('./content_hash');
@@ -107,11 +107,11 @@ async function doLogin() {
hash: hash
});
- if (sourceIdService.isLocalSourceId(resp.sourceId)) {
- throw new Error(`Sync server has source ID ${resp.sourceId} which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`);
+ if (resp.instanceId === instanceId) {
+ throw new Error(`Sync server has member ID ${resp.instanceId} which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`);
}
- syncContext.sourceId = resp.sourceId;
+ syncContext.instanceId = resp.instanceId;
const lastSyncedPull = getLastSyncedPull();
@@ -131,46 +131,45 @@ async function pullChanges(syncContext) {
while (true) {
const lastSyncedPull = getLastSyncedPull();
- const changesUri = '/api/sync/changed?lastEntityChangeId=' + lastSyncedPull;
+ const changesUri = `/api/sync/changed?instanceId=${instanceId}&lastEntityChangeId=${lastSyncedPull}`;
const startDate = Date.now();
const resp = await syncRequest(syncContext, 'GET', changesUri);
+ const {entityChanges, lastEntityChangeId} = resp;
+
+ outstandingPullCount = resp.outstandingPullCount;
const pulledDate = Date.now();
- outstandingPullCount = Math.max(0, resp.maxEntityChangeId - lastSyncedPull);
-
- const {entityChanges} = resp;
-
- if (entityChanges.length === 0) {
- break;
- }
-
sql.transactional(() => {
for (const {entityChange, entity} of entityChanges) {
const changeAppliedAlready = entityChange.changeId
&& !!sql.getValue("SELECT id FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
- if (!changeAppliedAlready && !sourceIdService.isLocalSourceId(entityChange.sourceId)) {
+ if (!changeAppliedAlready) {
if (!atLeastOnePullApplied) { // send only for first
ws.syncPullInProgress();
atLeastOnePullApplied = true;
}
- syncUpdateService.updateEntity(entityChange, entity);
+ syncUpdateService.updateEntity(entityChange, entity, syncContext.instanceId);
}
-
- outstandingPullCount = Math.max(0, resp.maxEntityChangeId - entityChange.id);
}
- setLastSyncedPull(entityChanges[entityChanges.length - 1].entityChange.id);
+ if (lastSyncedPull !== lastEntityChangeId) {
+ setLastSyncedPull(lastEntityChangeId);
+ }
});
- const sizeInKb = Math.round(JSON.stringify(resp).length / 1024);
+ if (entityChanges.length === 0) {
+ break;
+ } else {
+ const sizeInKb = Math.round(JSON.stringify(resp).length / 1024);
- log.info(`Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`);
+ log.info(`Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`);
+ }
}
log.info("Finished pull");
@@ -189,10 +188,9 @@ async function pushChanges(syncContext) {
}
const filteredEntityChanges = entityChanges.filter(entityChange => {
- if (entityChange.sourceId === syncContext.sourceId) {
+ if (entityChange.instanceId === syncContext.instanceId) {
// this may set lastSyncedPush beyond what's actually sent (because of size limit)
// so this is applied to the database only if there's no actual update
- // TODO: it would be better to simplify this somehow
lastSyncedPush = entityChange.id;
return false;
@@ -210,12 +208,12 @@ async function pushChanges(syncContext) {
continue;
}
- const entityChangesRecords = getEntityChangesRecords(filteredEntityChanges);
+ const entityChangesRecords = getEntityChangeRecords(filteredEntityChanges);
const startDate = new Date();
await syncRequest(syncContext, 'PUT', '/api/sync/update', {
- sourceId: sourceIdService.getCurrentSourceId(),
- entities: entityChangesRecords
+ entities: entityChangesRecords,
+ instanceId
});
ws.syncPushInProgress();
@@ -252,6 +250,11 @@ async function checkContentHash(syncContext) {
const failedChecks = contentHashService.checkContentHashes(resp.entityHashes);
+ process.exit(0);
+ throw new Error("AAAA");
+
+ return;
+
if (failedChecks.length > 0) {
// before requeuing sectors make sure the entity changes are correct
const consistencyChecks = require("./consistency_checks");
@@ -331,7 +334,7 @@ function getEntityChangeRow(entityName, entityId) {
}
}
-function getEntityChangesRecords(entityChanges) {
+function getEntityChangeRecords(entityChanges) {
const records = [];
let length = 0;
@@ -344,13 +347,6 @@ function getEntityChangesRecords(entityChanges) {
const entity = getEntityChangeRow(entityChange.entityName, entityChange.entityId);
- if (entityChange.entityName === 'options' && !entity.isSynced) {
- // if non-synced entities should count towards "lastSyncedPush"
- records.push({entityChange});
-
- continue;
- }
-
const record = { entityChange, entity };
records.push(record);
@@ -422,7 +418,7 @@ require("../becca/becca_loader").beccaLoaded.then(() => {
module.exports = {
sync,
login,
- getEntityChangesRecords,
+ getEntityChangeRecords,
getOutstandingPullCount,
getMaxEntityChangeId
};
diff --git a/src/services/sync_update.js b/src/services/sync_update.js
index 934517202..2492452e4 100644
--- a/src/services/sync_update.js
+++ b/src/services/sync_update.js
@@ -4,12 +4,12 @@ const entityChangesService = require('./entity_changes');
const eventService = require('./events');
const entityConstructor = require("../becca/entity_constructor");
-function updateEntity(entityChange, entityRow) {
+function updateEntity(entityChange, entityRow, instanceId) {
// can be undefined for options with isSynced=false
if (!entityRow) {
if (entityChange.isSynced) {
if (entityChange.isErased) {
- eraseEntity(entityChange);
+ eraseEntity(entityChange, instanceId);
}
else {
log.info(`Encountered synced non-erased entity change without entity: ${JSON.stringify(entityChange)}`);
@@ -23,8 +23,8 @@ function updateEntity(entityChange, entityRow) {
}
const updated = entityChange.entityName === 'note_reordering'
- ? updateNoteReordering(entityChange, entityRow)
- : updateNormalEntity(entityChange, entityRow);
+ ? updateNoteReordering(entityChange, entityRow, instanceId)
+ : updateNormalEntity(entityChange, entityRow, instanceId);
if (updated) {
if (entityRow.isDeleted) {
@@ -42,7 +42,7 @@ function updateEntity(entityChange, entityRow) {
}
}
-function updateNormalEntity(remoteEntityChange, entity) {
+function updateNormalEntity(remoteEntityChange, entity, instanceId) {
const localEntityChange = sql.getRow(`
SELECT utcDateChanged, hash, isErased
FROM entity_changes
@@ -54,7 +54,7 @@ function updateNormalEntity(remoteEntityChange, entity) {
sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId);
- entityChangesService.addEntityChange(remoteEntityChange);
+ entityChangesService.addEntityChangeWithinstanceId(remoteEntityChange, instanceId);
});
return true;
@@ -71,7 +71,7 @@ function updateNormalEntity(remoteEntityChange, entity) {
sql.transactional(() => {
sql.replace(remoteEntityChange.entityName, entity);
- entityChangesService.addEntityChange(remoteEntityChange);
+ entityChangesService.addEntityChangeWithinstanceId(remoteEntityChange, instanceId);
});
return true;
@@ -80,13 +80,13 @@ function updateNormalEntity(remoteEntityChange, entity) {
return false;
}
-function updateNoteReordering(entityChange, entity) {
+function updateNoteReordering(entityChange, entity, instanceId) {
sql.transactional(() => {
for (const key in entity) {
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
}
- entityChangesService.addEntityChange(entityChange);
+ entityChangesService.addEntityChangeWithinstanceId(entityChange, instanceId);
});
return true;
@@ -105,7 +105,7 @@ function handleContent(content) {
return content;
}
-function eraseEntity(entityChange) {
+function eraseEntity(entityChange, instanceId) {
const {entityName, entityId} = entityChange;
if (!["notes", "note_contents", "branches", "attributes", "note_revisions", "note_revision_contents"].includes(entityName)) {
@@ -119,7 +119,7 @@ function eraseEntity(entityChange) {
eventService.emit(eventService.ENTITY_DELETE_SYNCED, { entityName, entityId });
- entityChangesService.addEntityChange(entityChange, true);
+ entityChangesService.addEntityChangeWithinstanceId(entityChange, instanceId);
}
module.exports = {
diff --git a/src/services/tray.js b/src/services/tray.js
index c550973a2..7493e5c8b 100644
--- a/src/services/tray.js
+++ b/src/services/tray.js
@@ -1,7 +1,6 @@
const { Menu, Tray } = require('electron');
const path = require('path');
-const windowService = require("./window.js");
-const {getMainWindow} = require("./window.js");
+const windowService = require("./window");
const UPDATE_TRAY_EVENTS = [
'minimize', 'maximize', 'show', 'hide'
@@ -81,7 +80,7 @@ const updateTrayMenu = () => {
tray?.setContextMenu(contextMenu);
}
const changeVisibility = () => {
- const window = getMainWindow();
+ const window = windowService.getMainWindow();
if (isVisible) {
window.hide();
diff --git a/src/services/ws.js b/src/services/ws.js
index 3d090d21b..0231b200d 100644
--- a/src/services/ws.js
+++ b/src/services/ws.js
@@ -7,7 +7,7 @@ const config = require('./config');
const syncMutexService = require('./sync_mutex');
const protectedSessionService = require('./protected_session');
const becca = require("../becca/becca");
-const AbstractEntity = require("../becca/entities/abstract_entity.js");
+const AbstractEntity = require("../becca/entities/abstract_entity");
let webSocketServer;
let lastSyncedPush = null;
@@ -139,7 +139,7 @@ function fillInAdditionalProperties(entityChange) {
// entities with higher number can reference the entities with lower number
const ORDERING = {
- "api_tokens": 0,
+ "etapi_tokens": 0,
"attributes": 1,
"branches": 1,
"note_contents": 1,
diff --git a/src/share/routes.js b/src/share/routes.js
index e40a8222c..91096e55a 100644
--- a/src/share/routes.js
+++ b/src/share/routes.js
@@ -1,7 +1,7 @@
const shaca = require("./shaca/shaca");
const shacaLoader = require("./shaca/shaca_loader");
const shareRoot = require("./share_root");
-const contentRenderer = require("./content_renderer.js");
+const contentRenderer = require("./content_renderer");
function getSharedSubTreeRoot(note) {
if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
diff --git a/src/share/shaca/shaca_loader.js b/src/share/shaca/shaca_loader.js
index 8162f674a..92642f173 100644
--- a/src/share/shaca/shaca_loader.js
+++ b/src/share/shaca/shaca_loader.js
@@ -1,7 +1,7 @@
"use strict";
const sql = require('../sql');
-const shaca = require('./shaca.js');
+const shaca = require('./shaca');
const log = require('../../services/log');
const Note = require('./entities/note');
const Branch = require('./entities/branch');
diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs
index bdf73d471..06c50e090 100644
--- a/src/views/desktop.ejs
+++ b/src/views/desktop.ejs
@@ -39,6 +39,7 @@
<%- include('dialogs/include_note.ejs') %>
<%- include('dialogs/sort_child_notes.ejs') %>
<%- include('dialogs/delete_notes.ejs') %>
+<%- include('dialogs/password_not_set.ejs') %>
+
+
+