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 ce8c1a85c0..471d1deb18 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 0000000000..88bcbe6d5a
--- /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 34acfdaa55..c4e03b6b1f 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 a4822b8cf3..3902cf2994 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 f48ecef4f0..a84477da64 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 09eb698ff6..913aeb1d22 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 d63de54bb6..bfe83c3f6c 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 7c76408720..17347704b2 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 db8bc2d07d..bcb8b5301f 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 8fb53549c5..b6f0dbc8e4 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 6be2fef6af..036bc0e068 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 d2b4b4fb50..7d1c9bf3f8 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 120841183d..8b5a33745b 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 d3ac6f0491..4aaf9d70c4 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 295936d12a..e0c7d07ddf 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 4622770216..213787b106 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 288b69eb31..12d300b4f0 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 85d541c957..d7a396a8eb 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 583d630275..da6d24e616 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 e951fbbdce..39533183cd 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 785786e920..25aa227741 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 8a02096762..e82ba6d686 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 83c77d9ced..f965a4c016 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 42a5b5c93a..5fac6f97cf 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 489229a4b1..db6a2068f7 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 0000000000..b6a2ebfa7c
--- /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 9e4cc318ad..fa8685d8a0 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 6b43bec7f2..3a62749a72 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 57c1549ded..3df70f68d3 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 3478c457f4..47466bd7e2 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 d3c6a8a07e..4e2773f37f 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 d2ec74b2e1..dbaf44807e 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 1814cf8494..04ae3e55a8 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 a928540d24..3276e02a51 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 6a1abe66a4..04fe8cb028 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 b5837059a7..3431f34311 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 1b65257c47..31a84de8db 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 29ef3a3167..0cd8f8384c 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 2e6c6bfe96..73c6932198 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 d6d8567258..89f90a7d28 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 f43ead5947..948a1d4702 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 b06e713284..0000000000
--- 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 7f594fdda8..29ea067780 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 6198d9becc..b7ba1fff6a 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 33f2e730ce..787db3be58 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 254032e1b2..b595791c07 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 b02b4c5382..7e040b2693 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 0000000000..145e6d0895
--- /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 0000000000..4ee187b015
--- /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 c7cd68b3db..304b406dcc 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 da883ad27a..b9abb526b3 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 8ee3805663..5744148c3d 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 0000000000..f88de5062d
--- /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 3033ab0f71..30e7d9e09b 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 e59eceeaa7..1079fe5a1c 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 965a2b10bd..569f773b89 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 ac92a2c02a..02d2cc3bbf 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 37766ca150..c8729e3652 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 271f8ce3c0..eb176bebe7 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 f5917da8ef..0000000000
--- 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 2c572072d8..91dfc8ff83 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 76ebb52efd..7b5ed8bc06 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 b712e35a9a..ae71390d4f 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 d764d1ba8c..dffd5d76f5 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 9345172029..2492452e45 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 c550973a2a..7493e5c8b4 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 3d090d21be..0231b200d5 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 e40a8222c3..91096e55af 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 8162f674a3..92642f1730 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 bdf73d4717..06c50e0907 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') %>
+
+
+