Merge remote-tracking branch 'origin/next50'

This commit is contained in:
zadam 2022-01-10 19:53:36 +01:00
commit 916ff5f2ee
138 changed files with 3875 additions and 11959 deletions

2
.idea/misc.xml generated
View File

@ -3,7 +3,7 @@
<component name="JavaScriptSettings"> <component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" /> <option name="languageLevel" value="ES6" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" project-jdk-name="openjdk-16" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

3
TODO
View File

@ -1,3 +0,0 @@
- new icon
- polish becca entities API
- separate private and public APIs in becca entities

View File

@ -0,0 +1 @@
DELETE FROM options WHERE name = 'username';

View File

@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS "etapi_tokens"
(
etapiTokenId TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
tokenHash TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
utcDateModified TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0);
INSERT INTO etapi_tokens (etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified, isDeleted)
SELECT apiTokenId, 'Trilium Sender', token, utcDateCreated, utcDateCreated, isDeleted FROM api_tokens;
DROP TABLE api_tokens;

View File

@ -0,0 +1,10 @@
module.exports = () => {
const sql = require('../../src/services/sql');
const crypto = require('crypto');
for (const {etapiTokenId, token} of sql.getRows("SELECT etapiTokenId, tokenHash AS token FROM etapi_tokens")) {
const tokenHash = crypto.createHash('sha256').update(token).digest('base64');
sql.execute(`UPDATE etapi_tokens SET tokenHash = ? WHERE etapiTokenId = ?`, [tokenHash, etapiTokenId]);
}
};

View File

@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS "mig_entity_changes" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`hash` TEXT NOT NULL,
`isErased` INT NOT NULL,
`changeId` TEXT NOT NULL,
`componentId` TEXT NOT NULL,
`instanceId` TEXT NOT NULL,
`isSynced` INTEGER NOT NULL,
`utcDateChanged` TEXT NOT NULL
);
INSERT INTO mig_entity_changes (id, entityName, entityId, hash, isErased, changeId, componentId, instanceId, isSynced, utcDateChanged)
SELECT id, entityName, entityId, hash, isErased, changeId, '', '', isSynced, utcDateChanged FROM entity_changes;
DROP TABLE entity_changes;
ALTER TABLE mig_entity_changes RENAME TO entity_changes;
CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" (
`entityName`,
`entityId`
);

View File

@ -0,0 +1 @@
CREATE INDEX `IDX_entity_changes_changeId` ON `entity_changes` (`changeId`);

View File

@ -5,14 +5,16 @@ CREATE TABLE IF NOT EXISTS "entity_changes" (
`hash` TEXT NOT NULL, `hash` TEXT NOT NULL,
`isErased` INT NOT NULL, `isErased` INT NOT NULL,
`changeId` TEXT NOT NULL, `changeId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL, `componentId` TEXT NOT NULL,
`instanceId` TEXT NOT NULL,
`isSynced` INTEGER NOT NULL, `isSynced` INTEGER NOT NULL,
`utcDateChanged` TEXT NOT NULL `utcDateChanged` TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS "api_tokens" CREATE TABLE IF NOT EXISTS "etapi_tokens"
( (
apiTokenId TEXT PRIMARY KEY NOT NULL, etapiTokenId TEXT PRIMARY KEY NOT NULL,
token TEXT NOT NULL, name TEXT NOT NULL,
tokenHash TEXT NOT NULL,
utcDateCreated TEXT NOT NULL, utcDateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0); isDeleted INT NOT NULL DEFAULT 0);
CREATE TABLE IF NOT EXISTS "branches" ( CREATE TABLE IF NOT EXISTS "branches" (
@ -96,6 +98,7 @@ CREATE INDEX `IDX_note_revisions_utcDateCreated` ON `note_revisions` (`utcDateCr
CREATE INDEX `IDX_note_revisions_utcDateLastEdited` ON `note_revisions` (`utcDateLastEdited`); CREATE INDEX `IDX_note_revisions_utcDateLastEdited` ON `note_revisions` (`utcDateLastEdited`);
CREATE INDEX `IDX_note_revisions_dateCreated` ON `note_revisions` (`dateCreated`); CREATE INDEX `IDX_note_revisions_dateCreated` ON `note_revisions` (`dateCreated`);
CREATE INDEX `IDX_note_revisions_dateLastEdited` ON `note_revisions` (`dateLastEdited`); CREATE INDEX `IDX_note_revisions_dateLastEdited` ON `note_revisions` (`dateLastEdited`);
CREATE INDEX `IDX_entity_changes_changeId` ON `entity_changes` (`changeId`);
CREATE INDEX IDX_attributes_name_value CREATE INDEX IDX_attributes_name_value
on attributes (name, value); on attributes (name, value);
CREATE INDEX IDX_attributes_noteId_index CREATE INDEX IDX_attributes_noteId_index

View File

@ -0,0 +1,78 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
CodeMirror.defineOption("placeholder", "", function(cm, val, old) {
var prev = old && old != CodeMirror.Init;
if (val && !prev) {
cm.on("blur", onBlur);
cm.on("change", onChange);
cm.on("swapDoc", onChange);
CodeMirror.on(cm.getInputField(), "compositionupdate", cm.state.placeholderCompose = function() { onComposition(cm) })
onChange(cm);
} else if (!val && prev) {
cm.off("blur", onBlur);
cm.off("change", onChange);
cm.off("swapDoc", onChange);
CodeMirror.off(cm.getInputField(), "compositionupdate", cm.state.placeholderCompose)
clearPlaceholder(cm);
var wrapper = cm.getWrapperElement();
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "");
}
if (val && !cm.hasFocus()) onBlur(cm);
});
function clearPlaceholder(cm) {
if (cm.state.placeholder) {
cm.state.placeholder.parentNode.removeChild(cm.state.placeholder);
cm.state.placeholder = null;
}
}
function setPlaceholder(cm) {
clearPlaceholder(cm);
var elt = cm.state.placeholder = document.createElement("pre");
elt.style.cssText = "height: 0; overflow: visible";
elt.style.direction = cm.getOption("direction");
elt.className = "CodeMirror-placeholder CodeMirror-line-like";
var placeHolder = cm.getOption("placeholder")
if (typeof placeHolder == "string") placeHolder = document.createTextNode(placeHolder)
elt.appendChild(placeHolder)
cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild);
}
function onComposition(cm) {
setTimeout(function() {
var empty = false
if (cm.lineCount() == 1) {
var input = cm.getInputField()
empty = input.nodeName == "TEXTAREA" ? !cm.getLine(0).length
: !/[^\u200b]/.test(input.querySelector(".CodeMirror-line").textContent)
}
if (empty) setPlaceholder(cm)
else clearPlaceholder(cm)
}, 20)
}
function onBlur(cm) {
if (isEmpty(cm)) setPlaceholder(cm);
}
function onChange(cm) {
var wrapper = cm.getWrapperElement(), empty = isEmpty(cm);
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : "");
if (empty) setPlaceholder(cm);
else clearPlaceholder(cm);
}
function isEmpty(cm) {
return (cm.lineCount() === 1) && (cm.getLine(0) === "");
}
});

11479
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -43,10 +43,10 @@
"@electron/remote": "2.0.1", "@electron/remote": "2.0.1",
"express": "4.17.2", "express": "4.17.2",
"express-partial-content": "^1.0.2", "express-partial-content": "^1.0.2",
"express-rate-limit": "5.5.1", "express-rate-limit": "6.0.5",
"express-session": "1.17.2", "express-session": "1.17.2",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"helmet": "4.6.0", "helmet": "5.0.1",
"html": "1.0.0", "html": "1.0.0",
"html2plaintext": "2.1.4", "html2plaintext": "2.1.4",
"http-proxy-agent": "5.0.0", "http-proxy-agent": "5.0.0",
@ -88,7 +88,7 @@
"electron-packager": "15.4.0", "electron-packager": "15.4.0",
"electron-rebuild": "3.2.5", "electron-rebuild": "3.2.5",
"esm": "3.2.25", "esm": "3.2.25",
"jasmine": "3.10.0", "jasmine": "4.0.1",
"jsdoc": "3.6.7", "jsdoc": "3.6.7",
"lorem-ipsum": "2.0.4", "lorem-ipsum": "2.0.4",
"rcedit": "3.0.1", "rcedit": "3.0.1",

View File

@ -1,4 +1,4 @@
const lex = require('../../src/services/search/services/lex.js'); const lex = require('../../src/services/search/services/lex');
describe("Lexer fulltext", () => { describe("Lexer fulltext", () => {
it("simple lexing", () => { it("simple lexing", () => {

View File

@ -1,7 +1,7 @@
const Note = require('../../src/becca/entities/note.js'); const Note = require('../../src/becca/entities/note');
const Branch = require('../../src/becca/entities/branch.js'); const Branch = require('../../src/becca/entities/branch');
const Attribute = require('../../src/becca/entities/attribute.js'); const Attribute = require('../../src/becca/entities/attribute');
const becca = require('../../src/becca/becca.js'); const becca = require('../../src/becca/becca');
const randtoken = require('rand-token').generator({source: 'crypto'}); const randtoken = require('rand-token').generator({source: 'crypto'});
/** @returns {Note} */ /** @returns {Note} */

View File

@ -1,4 +1,4 @@
const handleParens = require('../../src/services/search/services/handle_parens.js'); const handleParens = require('../../src/services/search/services/handle_parens');
describe("Parens handler", () => { describe("Parens handler", () => {
it("handles parens", () => { it("handles parens", () => {

View File

@ -1,5 +1,5 @@
const SearchContext = require("../../src/services/search/search_context.js"); const SearchContext = require("../../src/services/search/search_context");
const parse = require('../../src/services/search/services/parse.js'); const parse = require('../../src/services/search/services/parse');
function tokens(toks, cur = 0) { function tokens(toks, cur = 0) {
return toks.map(arg => { return toks.map(arg => {

View File

@ -1,10 +1,10 @@
const searchService = require('../../src/services/search/services/search.js'); const searchService = require('../../src/services/search/services/search');
const Note = require('../../src/becca/entities/note.js'); const Note = require('../../src/becca/entities/note');
const Branch = require('../../src/becca/entities/branch.js'); const Branch = require('../../src/becca/entities/branch');
const SearchContext = require('../../src/services/search/search_context.js'); const SearchContext = require('../../src/services/search/search_context');
const dateUtils = require('../../src/services/date_utils.js'); const dateUtils = require('../../src/services/date_utils');
const becca = require('../../src/becca/becca.js'); const becca = require('../../src/becca/becca');
const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking.js'); const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking');
describe("Search", () => { describe("Search", () => {
let rootNote; let rootNote;

View File

@ -1,7 +1,7 @@
const {note} = require('./note_cache_mocking.js'); const {note} = require('./note_cache_mocking');
const ValueExtractor = require('../../src/services/search/value_extractor.js'); const ValueExtractor = require('../../src/services/search/value_extractor');
const becca = require('../../src/becca/becca.js'); const becca = require('../../src/becca/becca');
const SearchContext = require("../../src/services/search/search_context.js"); const SearchContext = require("../../src/services/search/search_context");
const dsc = new SearchContext(); const dsc = new SearchContext();

View File

@ -11,7 +11,7 @@ const sessionSecret = require('./services/session_secret');
const dataDir = require('./services/data_dir'); const dataDir = require('./services/data_dir');
const utils = require('./services/utils'); const utils = require('./services/utils');
require('./services/handlers'); require('./services/handlers');
require('./becca/becca_loader.js'); require('./becca/becca_loader');
const app = express(); const app = express();

View File

@ -1,7 +1,8 @@
"use strict"; "use strict";
const sql = require("../services/sql.js"); const sql = require("../services/sql");
const NoteSet = require("../services/search/note_set"); const NoteSet = require("../services/search/note_set");
const EtapiToken = require("./entities/etapi_token");
/** /**
* Becca is a backend cache of all notes, branches and attributes. There's a similar frontend cache Froca. * Becca is a backend cache of all notes, branches and attributes. There's a similar frontend cache Froca.
@ -24,6 +25,8 @@ class Becca {
this.attributeIndex = {}; this.attributeIndex = {};
/** @type {Object.<String, Option>} */ /** @type {Object.<String, Option>} */
this.options = {}; this.options = {};
/** @type {Object.<String, EtapiToken>} */
this.etapiTokens = {};
this.loaded = false; this.loaded = false;
} }
@ -64,10 +67,12 @@ class Becca {
this.dirtyNoteSetCache(); this.dirtyNoteSetCache();
} }
/** @returns {Note|null} */
getNote(noteId) { getNote(noteId) {
return this.notes[noteId]; return this.notes[noteId];
} }
/** @returns {Note[]} */
getNotes(noteIds, ignoreMissing = false) { getNotes(noteIds, ignoreMissing = false) {
const filteredNotes = []; const filteredNotes = [];
@ -88,29 +93,44 @@ class Becca {
return filteredNotes; return filteredNotes;
} }
/** @returns {Branch|null} */
getBranch(branchId) { getBranch(branchId) {
return this.branches[branchId]; return this.branches[branchId];
} }
/** @returns {Attribute|null} */
getAttribute(attributeId) { getAttribute(attributeId) {
return this.attributes[attributeId]; return this.attributes[attributeId];
} }
/** @returns {Branch|null} */
getBranchFromChildAndParent(childNoteId, parentNoteId) { getBranchFromChildAndParent(childNoteId, parentNoteId) {
return this.childParentToBranch[`${childNoteId}-${parentNoteId}`]; return this.childParentToBranch[`${childNoteId}-${parentNoteId}`];
} }
/** @returns {NoteRevision|null} */
getNoteRevision(noteRevisionId) { getNoteRevision(noteRevisionId) {
const row = sql.getRow("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [noteRevisionId]); const row = sql.getRow("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [noteRevisionId]);
const NoteRevision = require("./entities/note_revision.js"); // avoiding circular dependency problems const NoteRevision = require("./entities/note_revision"); // avoiding circular dependency problems
return row ? new NoteRevision(row) : null; return row ? new NoteRevision(row) : null;
} }
/** @returns {Option|null} */
getOption(name) { getOption(name) {
return this.options[name]; return this.options[name];
} }
/** @returns {EtapiToken[]} */
getEtapiTokens() {
return Object.values(this.etapiTokens);
}
/** @returns {EtapiToken|null} */
getEtapiToken(etapiTokenId) {
return this.etapiTokens[etapiTokenId];
}
getEntity(entityName, entityId) { getEntity(entityName, entityId) {
if (!entityName || !entityId) { if (!entityName || !entityId) {
return null; return null;
@ -130,17 +150,19 @@ class Becca {
return this[camelCaseEntityName][entityId]; return this[camelCaseEntityName][entityId];
} }
/** @returns {RecentNote[]} */
getRecentNotesFromQuery(query, params = []) { getRecentNotesFromQuery(query, params = []) {
const rows = sql.getRows(query, params); const rows = sql.getRows(query, params);
const RecentNote = require("./entities/recent_note.js"); // avoiding circular dependency problems const RecentNote = require("./entities/recent_note"); // avoiding circular dependency problems
return rows.map(row => new RecentNote(row)); return rows.map(row => new RecentNote(row));
} }
/** @returns {NoteRevision[]} */
getNoteRevisionsFromQuery(query, params = []) { getNoteRevisionsFromQuery(query, params = []) {
const rows = sql.getRows(query, params); const rows = sql.getRows(query, params);
const NoteRevision = require("./entities/note_revision.js"); // avoiding circular dependency problems const NoteRevision = require("./entities/note_revision"); // avoiding circular dependency problems
return rows.map(row => new NoteRevision(row)); return rows.map(row => new NoteRevision(row));
} }

View File

@ -9,6 +9,7 @@ const Note = require('./entities/note');
const Branch = require('./entities/branch'); const Branch = require('./entities/branch');
const Attribute = require('./entities/attribute'); const Attribute = require('./entities/attribute');
const Option = require('./entities/option'); const Option = require('./entities/option');
const EtapiToken = require("./entities/etapi_token");
const cls = require("../services/cls"); const cls = require("../services/cls");
const entityConstructor = require("../becca/entity_constructor"); const entityConstructor = require("../becca/entity_constructor");
@ -45,6 +46,10 @@ function load() {
new Option(row); new Option(row);
} }
for (const row of sql.getRows(`SELECT etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified FROM etapi_tokens WHERE isDeleted = 0`)) {
new EtapiToken(row);
}
for (const noteId in becca.notes) { for (const noteId in becca.notes) {
becca.notes[noteId].sortParents(); becca.notes[noteId].sortParents();
} }
@ -75,7 +80,7 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entity
return; return;
} }
if (["notes", "branches", "attributes"].includes(entityName)) { if (["notes", "branches", "attributes", "etapi_tokens"].includes(entityName)) {
const EntityClass = entityConstructor.getEntityFromEntityName(entityName); const EntityClass = entityConstructor.getEntityFromEntityName(entityName);
const primaryKeyName = EntityClass.primaryKeyName; const primaryKeyName = EntityClass.primaryKeyName;
@ -112,6 +117,8 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENT
branchDeleted(entityId); branchDeleted(entityId);
} else if (entityName === 'attributes') { } else if (entityName === 'attributes') {
attributeDeleted(entityId); attributeDeleted(entityId);
} else if (entityName === 'etapi_tokens') {
etapiTokenDeleted(entityId);
} }
}); });
@ -220,6 +227,10 @@ function noteReorderingUpdated(branchIdList) {
} }
} }
function etapiTokenDeleted(etapiTokenId) {
delete becca.etapiTokens[etapiTokenId];
}
eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => { eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => {
try { try {
becca.decryptProtectedNotes(); becca.decryptProtectedNotes();

View File

@ -40,7 +40,7 @@ class AbstractEntity {
get becca() { get becca() {
if (!becca) { if (!becca) {
becca = require('../becca.js'); becca = require('../becca');
} }
return becca; return becca;
@ -116,6 +116,19 @@ class AbstractEntity {
eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this }); eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this });
} }
markAsDeletedSimple() {
const entityId = this[this.constructor.primaryKeyName];
const entityName = this.constructor.entityName;
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ?
WHERE ${this.constructor.primaryKeyName} = ?`,
[dateUtils.utcNowDateTime(), entityId]);
this.addEntityChange(true);
eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this });
}
} }
module.exports = AbstractEntity; module.exports = AbstractEntity;

View File

@ -1,34 +0,0 @@
"use strict";
const dateUtils = require('../../services/date_utils.js');
const AbstractEntity = require("./abstract_entity.js");
/**
* ApiToken is an entity representing token used to authenticate against Trilium API from client applications. Currently used only by Trilium Sender.
*/
class ApiToken extends AbstractEntity {
static get entityName() { return "api_tokens"; }
static get primaryKeyName() { return "apiTokenId"; }
static get hashedProperties() { return ["apiTokenId", "token", "utcDateCreated"]; }
constructor(row) {
super();
/** @type {string} */
this.apiTokenId = row.apiTokenId;
/** @type {string} */
this.token = row.token;
/** @type {string} */
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
}
getPojo() {
return {
apiTokenId: this.apiTokenId,
token: this.token,
utcDateCreated: this.utcDateCreated
}
}
}
module.exports = ApiToken;

View File

@ -1,9 +1,9 @@
"use strict"; "use strict";
const Note = require('./note.js'); const Note = require('./note');
const AbstractEntity = require("./abstract_entity.js"); const AbstractEntity = require("./abstract_entity");
const sql = require("../../services/sql.js"); const sql = require("../../services/sql");
const dateUtils = require("../../services/date_utils.js"); const dateUtils = require("../../services/date_utils");
const promotedAttributeDefinitionParser = require("../../services/promoted_attribute_definition_parser"); const promotedAttributeDefinitionParser = require("../../services/promoted_attribute_definition_parser");
/** /**

View File

@ -1,9 +1,9 @@
"use strict"; "use strict";
const Note = require('./note.js'); const Note = require('./note');
const AbstractEntity = require("./abstract_entity.js"); const AbstractEntity = require("./abstract_entity");
const sql = require("../../services/sql.js"); const sql = require("../../services/sql");
const dateUtils = require("../../services/date_utils.js"); const dateUtils = require("../../services/date_utils");
/** /**
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple * Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple

View File

@ -0,0 +1,72 @@
"use strict";
const dateUtils = require('../../services/date_utils');
const AbstractEntity = require("./abstract_entity");
const sql = require("../../services/sql.js");
/**
* EtapiToken is an entity representing token used to authenticate against Trilium REST API from client applications.
* Used by:
* - Trilium Sender
* - ETAPI clients
*/
class EtapiToken extends AbstractEntity {
static get entityName() { return "etapi_tokens"; }
static get primaryKeyName() { return "etapiTokenId"; }
static get hashedProperties() { return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"]; }
constructor(row) {
super();
if (!row) {
return;
}
this.updateFromRow(row);
this.init();
}
updateFromRow(row) {
/** @type {string} */
this.etapiTokenId = row.etapiTokenId;
/** @type {string} */
this.name = row.name;
/** @type {string} */
this.tokenHash = row.tokenHash;
/** @type {string} */
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
/** @type {string} */
this.utcDateModified = row.utcDateModified || this.utcDateCreated;
/** @type {boolean} */
this.isDeleted = !!row.isDeleted;
this.becca.etapiTokens[this.etapiTokenId] = this;
}
init() {
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
getPojo() {
return {
etapiTokenId: this.etapiTokenId,
name: this.name,
tokenHash: this.tokenHash,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified,
isDeleted: this.isDeleted
}
}
beforeSaving() {
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
module.exports = EtapiToken;

View File

@ -6,8 +6,8 @@ const sql = require('../../services/sql');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const entityChangesService = require('../../services/entity_changes'); const entityChangesService = require('../../services/entity_changes');
const AbstractEntity = require("./abstract_entity.js"); const AbstractEntity = require("./abstract_entity");
const NoteRevision = require("./note_revision.js"); const NoteRevision = require("./note_revision");
const LABEL = 'label'; const LABEL = 'label';
const RELATION = 'relation'; const RELATION = 'relation';
@ -984,7 +984,7 @@ class Note extends AbstractEntity {
} }
} }
else { else {
const Attribute = require("./attribute.js"); const Attribute = require("./attribute");
new Attribute({ new Attribute({
noteId: this.noteId, noteId: this.noteId,
@ -1016,7 +1016,7 @@ class Note extends AbstractEntity {
* @return {Attribute} * @return {Attribute}
*/ */
addAttribute(type, name, value = "", isInheritable = false, position = 1000) { addAttribute(type, name, value = "", isInheritable = false, position = 1000) {
const Attribute = require("./attribute.js"); const Attribute = require("./attribute");
return new Attribute({ return new Attribute({
noteId: this.noteId, noteId: this.noteId,

View File

@ -4,9 +4,9 @@ const protectedSessionService = require('../../services/protected_session');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const becca = require('../becca.js'); const becca = require('../becca');
const entityChangesService = require('../../services/entity_changes'); const entityChangesService = require('../../services/entity_changes');
const AbstractEntity = require("./abstract_entity.js"); const AbstractEntity = require("./abstract_entity");
/** /**
* NoteRevision represents snapshot of note's title and content at some point in the past. * NoteRevision represents snapshot of note's title and content at some point in the past.

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
const dateUtils = require('../../services/date_utils.js'); const dateUtils = require('../../services/date_utils');
const AbstractEntity = require("./abstract_entity.js"); const AbstractEntity = require("./abstract_entity");
/** /**
* Option represents name-value pair, either directly configurable by the user or some system property. * Option represents name-value pair, either directly configurable by the user or some system property.

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
const dateUtils = require('../../services/date_utils.js'); const dateUtils = require('../../services/date_utils');
const AbstractEntity = require("./abstract_entity.js"); const AbstractEntity = require("./abstract_entity");
/** /**
* RecentNote represents recently visited note. * RecentNote represents recently visited note.

View File

@ -3,7 +3,7 @@ const NoteRevision = require('./entities/note_revision');
const Branch = require('./entities/branch'); const Branch = require('./entities/branch');
const Attribute = require('./entities/attribute'); const Attribute = require('./entities/attribute');
const RecentNote = require('./entities/recent_note'); const RecentNote = require('./entities/recent_note');
const ApiToken = require('./entities/api_token'); const EtapiToken = require('./entities/etapi_token');
const Option = require('./entities/option'); const Option = require('./entities/option');
const ENTITY_NAME_TO_ENTITY = { const ENTITY_NAME_TO_ENTITY = {
@ -14,7 +14,7 @@ const ENTITY_NAME_TO_ENTITY = {
"note_revisions": NoteRevision, "note_revisions": NoteRevision,
"note_revision_contents": NoteRevision, "note_revision_contents": NoteRevision,
"recent_notes": RecentNote, "recent_notes": RecentNote,
"api_tokens": ApiToken, "etapi_tokens": EtapiToken,
"options": Option "options": Option
}; };

View File

@ -1,4 +1,4 @@
const becca = require('./becca.js'); const becca = require('./becca');
const log = require('../services/log'); const log = require('../services/log');
const beccaService = require('./becca_service.js'); const beccaService = require('./becca_service.js');
const dateUtils = require('../services/date_utils'); const dateUtils = require('../services/date_utils');

64
src/etapi/attributes.js Normal file
View File

@ -0,0 +1,64 @@
const becca = require("../becca/becca");
const eu = require("./etapi_utils");
const mappers = require("./mappers");
const attributeService = require("../services/attributes");
const validators = require("./validators");
function register(router) {
eu.route(router, 'get', '/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
res.json(mappers.mapAttributeToPojo(attribute));
});
eu.route(router, 'post' ,'/etapi/attributes', (req, res, next) => {
const params = req.body;
eu.getAndCheckNote(params.noteId);
if (params.type === 'relation') {
eu.getAndCheckNote(params.value);
}
if (params.type !== 'relation' && params.type !== 'label') {
throw new eu.EtapiError(400, eu.GENERIC_CODE, `Only "relation" and "label" are supported attribute types, "${params.type}" given.`);
}
try {
const attr = attributeService.createAttribute(params);
res.json(mappers.mapAttributeToPojo(attr));
}
catch (e) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'value': validators.isString
};
eu.route(router, 'patch' ,'/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
res.json(mappers.mapAttributeToPojo(attribute));
});
eu.route(router, 'delete' ,'/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = becca.getAttribute(req.params.attributeId);
if (!attribute || attribute.isDeleted) {
return res.sendStatus(204);
}
attribute.markAsDeleted();
res.sendStatus(204);
});
}
module.exports = {
register
};

43
src/etapi/auth.js Normal file
View File

@ -0,0 +1,43 @@
const becca = require("../becca/becca");
const eu = require("./etapi_utils");
const passwordEncryptionService = require("../services/password_encryption.js");
const etapiTokenService = require("../services/etapi_tokens.js");
function register(router) {
eu.NOT_AUTHENTICATED_ROUTE(router, 'post', '/etapi/auth/login', (req, res, next) => {
const {password, tokenName} = req.body;
if (!passwordEncryptionService.verifyPassword(password)) {
throw new eu.EtapiError(401, "WRONG_PASSWORD", "Wrong password.");
}
const {authToken} = etapiTokenService.createToken(tokenName || "ETAPI login");
res.json({
authToken
});
});
eu.route(router, 'post', '/etapi/auth/logout', (req, res, next) => {
const parsed = etapiTokenService.parseAuthToken(req.headers.authorization);
if (!parsed || !parsed.etapiTokenId) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, "Cannot logout this token.");
}
const etapiToken = becca.getEtapiToken(parsed.etapiTokenId);
if (!etapiToken) {
// shouldn't happen since this already passed auth validation
throw new Error(`Cannot find the token ${parsed.etapiTokenId}.`);
}
etapiToken.markAsDeletedSimple();
res.sendStatus(204);
});
}
module.exports = {
register
}

80
src/etapi/branches.js Normal file
View File

@ -0,0 +1,80 @@
const becca = require("../becca/becca");
const eu = require("./etapi_utils");
const mappers = require("./mappers");
const Branch = require("../becca/entities/branch");
const noteService = require("../services/notes");
const TaskContext = require("../services/task_context");
const entityChangesService = require("../services/entity_changes");
const validators = require("./validators");
function register(router) {
eu.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
res.json(mappers.mapBranchToPojo(branch));
});
eu.route(router, 'post' ,'/etapi/branches', (req, res, next) => {
const params = req.body;
eu.getAndCheckNote(params.noteId);
eu.getAndCheckNote(params.parentNoteId);
const existing = becca.getBranchFromChildAndParent(params.noteId, params.parentNoteId);
if (existing) {
existing.notePosition = params.notePosition;
existing.prefix = params.prefix;
existing.save();
return res.json(mappers.mapBranchToPojo(existing));
}
try {
const branch = new Branch(params).save();
res.json(mappers.mapBranchToPojo(branch));
}
catch (e) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'notePosition': validators.isInteger,
'prefix': validators.isStringOrNull,
'isExpanded': validators.isBoolean
};
eu.route(router, 'patch' ,'/etapi/branches/:branchId', (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
res.json(mappers.mapBranchToPojo(branch));
});
eu.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => {
const branch = becca.getBranch(req.params.branchId);
if (!branch || branch.isDeleted) {
return res.sendStatus(204);
}
noteService.deleteBranch(branch, null, new TaskContext('no-progress-reporting'));
res.sendStatus(204);
});
eu.route(router, 'post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
eu.getAndCheckNote(req.params.parentNoteId);
entityChangesService.addNoteReorderingEntityChange(req.params.parentNoteId, "etapi");
res.sendStatus(204);
});
}
module.exports = {
register
};

View File

@ -0,0 +1,789 @@
openapi: "3.0.3"
info:
version: 1.0.0
title: ETAPI
description: External Trilium API
contact:
name: zadam
email: zadam.apps@gmail.com
url: https://github.com/zadam/trilium
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: http://localhost:37740/etapi
- url: http://localhost:8080/etapi
security:
- EtapiTokenAuth: []
paths:
/create-note:
post:
description: Create a note and place it into the note tree
operationId: createNote
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateNoteDef'
responses:
'200':
description: note created
content:
application/json:
schema:
properties:
note:
$ref: '#/components/schemas/Note'
description: Created note
branch:
$ref: '#/components/schemas/Branch'
description: Created branch
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/notes:
get:
description: Search notes
operationId: searchNotes
parameters:
- name: search
in: query
required: true
description: search query string as described in https://github.com/zadam/trilium/wiki/Search
schema:
type: string
examples:
fulltext:
summary: Fulltext search for keywords (not exact match)
value: 'towers tolkien'
fulltextExactMatch:
summary: Fulltext search for exact match (notice the double quotes)
value: '"Two Towers"'
fulltextWithLabel:
summary: Fulltext search for keyword AND matching label
value: 'towers #book'
- name: fastSearch
in: query
required: false
description: enable fast search (fulltext doesn't look into content)
schema:
type: boolean
default: false
- name: includeArchivedNotes
in: query
required: false
description: search by default ignores archived notes. Set to 'true' to includes archived notes into search results.
schema:
type: boolean
default: false
- name: ancestorNoteId
in: query
required: false
description: search only in a subtree identified by the subtree noteId. By default whole tree is searched.
schema:
$ref: '#/components/schemas/EntityId'
- name: ancestorDepth
in: query
required: false
description: define how deep in the tree should the notes be searched
schema:
type: string
examples:
directChildren:
summary: depth of exactly 1 (direct children) to the ancestor (root if not set)
value: eq1
grandGrandChildren:
summary: depth of exactly 3 to the ancestor (root if not set)
value: eq3
lessThan4:
summary: depth less than 4 (so 1, 2, 3) to the ancestor (root if not set)
value: lt4
greaterThan2:
summary: depth greater than 2 (so 3, 4, 5, 6...) to the ancestor (root if not set)
value: gt4
- name: orderBy
in: query
required: false
description: name of the property/label to order search results by
schema:
type: string
example:
- title
- '#publicationDate'
- isProtected
- isArchived
- dateCreated
- dateModified
- utcDateCreated
- utcDateModified
- parentCount
- childrenCount
- attributeCount
- labelCount
- ownedLabelCount
- relationCount
- ownedRelationCount
- relationCountIncludingLinks
- ownedRelationCountIncludingLinks
- targetRelationCount
- targetRelationCountIncludingLinks
- contentSize
- noteSize
- revisionCount
- name: orderDirection
in: query
required: false
description: order direction, ascending or descending
schema:
type: string
default: asc
enum:
- asc
- desc
- name: limit
in: query
required: false
description: limit the number of results you want to receive
schema:
type: integer
example: 10
- name: debug
in: query
required: false
description: set to true to get debug information in the response (search query parsing)
schema:
type: boolean
default: false
responses:
'200':
description: search response
content:
application/json:
schema:
$ref: '#/components/schemas/SearchResponse'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/notes/{noteId}:
parameters:
- name: noteId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
get:
description: Returns a note identified by its ID
operationId: getNoteById
responses:
'200':
description: note response
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
patch:
description: patch a note identified by the noteId with changes in the body
operationId: patchNoteById
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
responses:
'200':
description: update note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes a single note based on the noteId supplied
operationId: deleteNoteById
responses:
'204':
description: note deleted
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/branches/{branchId}:
parameters:
- name: branchId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
get:
description: Returns a branch identified by its ID
operationId: getBranchById
responses:
'200':
description: branch response
content:
application/json:
schema:
$ref: '#/components/schemas/Branch'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
description: create a branch (clone a note to a different location in the tree)
operationId: postBranch
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Branch'
responses:
'200':
description: update branch
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
patch:
description: patch a branch identified by the branchId with changes in the body
operationId: patchBranchById
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Branch'
responses:
'200':
description: update branch
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
description: >
deletes a branch based on the branchId supplied. If this is the last branch of the (child) note,
then the note is deleted as well.
operationId: deleteBranchById
responses:
'204':
description: branch deleted
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/attributes/{attributeId}:
parameters:
- name: attributeId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
get:
description: Returns an attribute identified by its ID
operationId: getAttributeById
responses:
'200':
description: attribute response
content:
application/json:
schema:
$ref: '#/components/schemas/Attribute'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
description: create an attribute for a given note
operationId: postAttribute
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Attribute'
responses:
'200':
description: update attribute
content:
application/json:
schema:
$ref: '#/components/schemas/Attribute'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
patch:
description: patch a attribute identified by the attributeId with changes in the body
operationId: patchAttributeById
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Attribute'
responses:
'200':
description: update attribute
content:
application/json:
schema:
$ref: '#/components/schemas/Attribute'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes a attribute based on the attributeId supplied.
operationId: deleteAttributeById
responses:
'204':
description: attribute deleted
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/refresh-note-ordering/{parentNoteId}:
parameters:
- name: parentNoteId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
post:
description: >
notePositions in branches are not automatically pushed to connected clients and need a specific instruction.
If you want your changes to be in effect immediately, call this service after setting branches' notePosition.
Note that you need to supply "parentNoteId" of branch(es) with changed positions.
operationId: postRefreshNoteOrdering
responses:
'204':
description: note ordering will be asynchronously updated in all connected clients
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/inbox/{date}:
get:
description: >
returns an "inbox" note, into which note can be created. Date will be used depending on whether the inbox
is a fixed note (identified with #inbox label) or a day note in a journal.
operationId: getInboxNote
parameters:
- name: date
in: path
required: true
schema:
type: string
format: date
example: 2022-02-22
responses:
'200':
description: inbox note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/calendar/days/{date}:
get:
description: returns a day note for a given date. Gets created if doesn't exist.
operationId: getDayNote
parameters:
- name: date
in: path
required: true
schema:
type: string
format: date
example: 2022-02-22
responses:
'200':
description: day note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/calendar/weeks/{date}:
get:
description: returns a week note for a given date. Gets created if doesn't exist.
operationId: getWeekNote
parameters:
- name: date
in: path
required: true
schema:
type: string
format: date
example: 2022-02-22
responses:
'200':
description: week note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/calendar/months/{month}:
get:
description: returns a week note for a given date. Gets created if doesn't exist.
operationId: getMonthNote
parameters:
- name: month
in: path
required: true
schema:
type: string
pattern: '[0-9]{4}-[0-9]{2}'
example: 2022-02
responses:
'200':
description: month note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/calendar/years/{year}:
get:
description: returns a week note for a given date. Gets created if doesn't exist.
operationId: getYearNote
parameters:
- name: year
in: path
required: true
schema:
type: string
pattern: '[0-9]{4}-[0-9]{2}'
example: 2022-02
responses:
'200':
description: year note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/auth/login:
post:
description: get an ETAPI token based on password for further use with ETAPI
operationId: login
security: [] # no token based auth for login endpoint
requestBody:
required: true
content:
application/json:
schema:
properties:
password:
type: string
description: user's password used to e.g. login to Trilium server and/or protect notes
responses:
'200':
description: auth token
content:
application/json:
schema:
properties:
authToken:
type: string
example: Bc4bFn0Ffiok_4NpbVCDnFz7B2WU+pdhW8B5Ne3DiR5wXrEyqdjgRIsk=
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/auth/logout:
post:
description: logout (delete/deactivate) an ETAPI token
operationId: logout
responses:
'204':
description: logout successful
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
securitySchemes:
EtapiTokenAuth:
type: apiKey
in: header
name: Authorization
schemas:
CreateNoteDef:
type: object
required:
- parentNoteId
- type
- title
- content
properties:
parentNoteId:
$ref: '#/components/schemas/EntityId'
description: Note ID of the parent note in the tree
type:
type: string
enum:
- text
- code
- file
- image
- search
- book
- relation-map
- render
mime:
type: string
description: this needs to be specified only for note types 'code', 'file', 'image'.
example: application/json
title:
type: string
content:
type: string
notePosition:
type: integer
description: >
Position of the note in the parent. Normal ordering is 10, 20, 30 ...
So if you want to create a note on the first position, use e.g. 5, for second position 15, for last e.g. 1000000
prefix:
type: string
description: >
Prefix is branch (placement) specific title prefix for the note.
Let's say you have your note placed into two different places in the tree,
but you want to change the title a bit in one of the placements. For this you can use prefix.
noteId:
$ref: '#/components/schemas/EntityId'
description: DON'T specify unless you want to force a specific noteId
branchId:
$ref: '#/components/schemas/EntityId'
description: DON'T specify unless you want to force a specific branchId
Note:
type: object
properties:
noteId:
$ref: '#/components/schemas/EntityId'
readOnly: true
title:
type: string
type:
type: string
enum: [text, code, book, image, file, mermaid, relation-map, render, search, note-map]
mime:
type: string
isProtected:
type: boolean
readOnly: true
attributes:
$ref: '#/components/schemas/AttributeList'
readOnly: true
parentNoteIds:
$ref: '#/components/schemas/EntityIdList'
readOnly: true
childNoteIds:
$ref: '#/components/schemas/EntityIdList'
readOnly: true
parentBranchIds:
$ref: '#/components/schemas/EntityIdList'
readOnly: true
childBranchIds:
$ref: '#/components/schemas/EntityIdList'
readOnly: true
dateCreated:
$ref: '#/components/schemas/LocalDateTime'
readOnly: true
dateModified:
$ref: '#/components/schemas/LocalDateTime'
readOnly: true
utcDateCreated:
$ref: '#/components/schemas/UtcDateTime'
readOnly: true
utcDateModified:
$ref: '#/components/schemas/UtcDateTime'
readOnly: true
Branch:
type: object
description: Branch places the note into the tree, it represents the relationship between a parent note and child note
required:
- noteId
- parentNoteId
properties:
branchId:
$ref: '#/components/schemas/EntityId'
readOnly: true
noteId:
$ref: '#/components/schemas/EntityId'
readOnly: true
description: identifies the child note
parentNoteId:
$ref: '#/components/schemas/EntityId'
readOnly: true
description: identifies the parent note
prefix:
type: string
notePosition:
type: integer
format: int32
isExanded:
type: boolean
utcDateModified:
$ref: '#/components/schemas/UtcDateTime'
readOnly: true
Attribute:
type: object
description: Attribute (Label, Relation) is a key-value record attached to a note.
required:
- noteId
properties:
attributeId:
$ref: '#/components/schemas/EntityId'
readOnly: true
noteId:
$ref: '#/components/schemas/EntityId'
readOnly: true
description: identifies the child note
type:
type: string
enum: [label, relation]
name:
type: string
pattern: '^[\p{L}\p{N}_:]+'
example: shareCss
value:
type: string
position:
type: integer
format: int32
isInheritable:
type: boolean
utcDateModified:
$ref: '#/components/schemas/UtcDateTime'
readOnly: true
AttributeList:
type: array
items:
$ref: '#/components/schemas/Attribute'
SearchResponse:
type: object
required:
- results
properties:
results:
type: array
items:
$ref: '#/components/schemas/Note'
debugInfo:
type: object
description: debugging info on parsing the search query enabled with &debug=true parameter
EntityId:
type: string
pattern: '[a-zA-Z0-9]{4,12}'
example: evnnmvHTCgIn
EntityIdList:
type: array
items:
$ref: '#/components/schemas/EntityId'
LocalDateTime:
type: string
pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}\+[0-9]{4}'
example: 2021-12-31 20:18:11.939+0100
UtcDateTime:
type: string
pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z'
example: 2021-12-31 19:18:11.939Z
Error:
type: object
required:
- status
- code
- message
properties:
status:
type: integer
format: int32
description: HTTP status, identical to the one given in HTTP response
example: 400
code:
type: string
description: stable string constant
example: NOTE_IS_PROTECTED
message:
type: string
description: Human readable error, potentially with more details,
example: Note 'evnnmvHTCgIn' is protected and cannot be modified through ETAPI

139
src/etapi/etapi_utils.js Normal file
View File

@ -0,0 +1,139 @@
const cls = require("../services/cls");
const sql = require("../services/sql");
const log = require("../services/log");
const becca = require("../becca/becca");
const etapiTokenService = require("../services/etapi_tokens.js");
const config = require("../services/config.js");
const GENERIC_CODE = "GENERIC";
const noAuthentication = config.General && config.General.noAuthentication === true;
class EtapiError extends Error {
constructor(statusCode, code, message) {
super();
this.statusCode = statusCode;
this.code = code;
this.message = message;
}
}
function sendError(res, statusCode, code, message) {
return res
.set('Content-Type', 'application/json')
.status(statusCode)
.send(JSON.stringify({
"status": statusCode,
"code": code,
"message": message
}));
}
function checkEtapiAuth(req, res, next) {
if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
next();
}
else {
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
}
}
function processRequest(req, res, routeHandler, next, method, path) {
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
cls.init(() => {
cls.set('componentId', "etapi");
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
const cb = () => routeHandler(req, res, next);
return sql.transactional(cb);
});
} catch (e) {
log.error(`${method} ${path} threw exception ${e.message} with stacktrace: ${e.stack}`);
if (e instanceof EtapiError) {
sendError(res, e.statusCode, e.code, e.message);
} else {
sendError(res, 500, GENERIC_CODE, e.message);
}
}
}
function route(router, method, path, routeHandler) {
router[method](path, checkEtapiAuth, (req, res, next) => processRequest(req, res, routeHandler, next, method, path));
}
function NOT_AUTHENTICATED_ROUTE(router, method, path, routeHandler) {
router[method](path, (req, res, next) => processRequest(req, res, routeHandler, next, method, path));
}
function getAndCheckNote(noteId) {
const note = becca.getNote(noteId);
if (note) {
return note;
}
else {
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found`);
}
}
function getAndCheckBranch(branchId) {
const branch = becca.getBranch(branchId);
if (branch) {
return branch;
}
else {
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found`);
}
}
function getAndCheckAttribute(attributeId) {
const attribute = becca.getAttribute(attributeId);
if (attribute) {
return attribute;
}
else {
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found`);
}
}
function validateAndPatch(entity, props, allowedProperties) {
for (const key of Object.keys(props)) {
if (!(key in allowedProperties)) {
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED_FOR_PATCH", `Property '${key}' is not allowed for PATCH.`);
}
else {
const validator = allowedProperties[key];
const validationResult = validator(props[key]);
if (validationResult) {
throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}`);
}
}
}
// validation passed, let's patch
for (const propName of Object.keys(props)) {
entity[propName] = props[propName];
}
entity.save();
}
module.exports = {
EtapiError,
sendError,
route,
NOT_AUTHENTICATED_ROUTE,
GENERIC_CODE,
validateAndPatch,
getAndCheckNote,
getAndCheckBranch,
getAndCheckAttribute
}

49
src/etapi/mappers.js Normal file
View File

@ -0,0 +1,49 @@
function mapNoteToPojo(note) {
return {
noteId: note.noteId,
isProtected: note.isProtected,
title: note.title,
type: note.type,
mime: note.mime,
dateCreated: note.dateCreated,
dateModified: note.dateModified,
utcDateCreated: note.utcDateCreated,
utcDateModified: note.utcDateModified,
parentNoteIds: note.getParentNotes().map(p => p.noteId),
childNoteIds: note.getChildNotes().map(ch => ch.noteId),
parentBranchIds: note.getParentBranches().map(p => p.branchId),
childBranchIds: note.getChildBranches().map(ch => ch.branchId),
attributes: note.getAttributes().map(attr => mapAttributeToPojo(attr))
};
}
function mapBranchToPojo(branch) {
return {
branchId: branch.branchId,
noteId: branch.noteId,
parentNoteId: branch.parentNoteId,
prefix: branch.prefix,
notePosition: branch.notePosition,
isExpanded: branch.isExpanded,
utcDateModified: branch.utcDateModified
};
}
function mapAttributeToPojo(attr) {
return {
attributeId: attr.attributeId,
noteId: attr.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
position: attr.position,
isInheritable: attr.isInheritable,
utcDateModified: attr.utcDateModified
};
}
module.exports = {
mapNoteToPojo,
mapBranchToPojo,
mapAttributeToPojo
};

181
src/etapi/notes.js Normal file
View File

@ -0,0 +1,181 @@
const becca = require("../becca/becca");
const utils = require("../services/utils");
const eu = require("./etapi_utils");
const mappers = require("./mappers");
const noteService = require("../services/notes");
const TaskContext = require("../services/task_context");
const validators = require("./validators");
const searchService = require("../services/search/services/search");
const SearchContext = require("../services/search/search_context");
function register(router) {
eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
const {search} = req.query;
if (!search?.trim()) {
throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory");
}
const searchParams = parseSearchParams(req);
const searchContext = new SearchContext(searchParams);
const searchResults = searchService.findResultsWithQuery(search, searchContext);
const foundNotes = searchResults.map(sr => becca.notes[sr.noteId]);
const resp = {
results: foundNotes.map(note => mappers.mapNoteToPojo(note))
};
if (searchContext.debugInfo) {
resp.debugInfo = searchContext.debugInfo;
}
res.json(resp);
});
eu.route(router, 'get', '/etapi/notes/:noteId', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'post' ,'/etapi/create-note', (req, res, next) => {
const params = req.body;
eu.getAndCheckNote(params.parentNoteId);
try {
const resp = noteService.createNewNote(params);
res.json({
note: mappers.mapNoteToPojo(resp.note),
branch: mappers.mapBranchToPojo(resp.branch)
});
}
catch (e) {
return eu.sendError(res, 400, eu.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'title': validators.isString,
'type': validators.isString,
'mime': validators.isString
};
eu.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId)
if (note.isProtected) {
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI`);
}
eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'delete' ,'/etapi/notes/:noteId', (req, res, next) => {
const {noteId} = req.params;
const note = becca.getNote(noteId);
if (!note || note.isDeleted) {
return res.sendStatus(204);
}
noteService.deleteNote(note, null, new TaskContext('no-progress-reporting'));
res.sendStatus(204);
});
eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime);
res.send(note.getContent());
});
eu.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
note.setContent(req.body);
return res.sendStatus(204);
});
}
function parseSearchParams(req) {
const rawSearchParams = {
'fastSearch': parseBoolean(req.query, 'fastSearch'),
'includeArchivedNotes': parseBoolean(req.query, 'includeArchivedNotes'),
'ancestorNoteId': req.query['ancestorNoteId'],
'ancestorDepth': parseInteger(req.query, 'ancestorDepth'),
'orderBy': req.query['orderBy'],
'orderDirection': parseOrderDirection(req.query, 'orderDirection'),
'limit': parseInteger(req.query, 'limit'),
'debug': parseBoolean(req.query, 'debug')
};
const searchParams = {};
for (const paramName of Object.keys(rawSearchParams)) {
if (rawSearchParams[paramName] !== undefined) {
searchParams[paramName] = rawSearchParams[paramName];
}
}
return searchParams;
}
const SEARCH_PARAM_ERROR = "SEARCH_PARAM_VALIDATION_ERROR";
function parseBoolean(obj, name) {
if (!(name in obj)) {
return undefined;
}
if (!['true', 'false'].includes(obj[name])) {
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'`);
}
return obj[name] === 'true';
}
function parseInteger(obj, name) {
if (!(name in obj)) {
return undefined;
}
const integer = parseInt(obj[name]);
if (!['asc', 'desc'].includes(obj[name])) {
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'`);
}
return integer;
}
function parseOrderDirection(obj, name) {
if (!(name in obj)) {
return undefined;
}
const integer = parseInt(obj[name]);
if (Number.isNaN(integer)) {
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse integer '${name}' value '${obj[name]}`);
}
return integer;
}
module.exports = {
register
};

20
src/etapi/spec.js Normal file
View File

@ -0,0 +1,20 @@
const fs = require('fs');
const path = require('path');
const specPath = path.join(__dirname, 'etapi.openapi.yaml');
let spec = null;
function register(router) {
router.get('/etapi/etapi.openapi.yaml', (req, res, next) => {
if (!spec) {
spec = fs.readFileSync(specPath, 'utf8');
}
res.header('Content-Type', 'text/plain'); // so that it displays in browser
res.status(200).send(spec);
});
}
module.exports = {
register
};

View File

@ -0,0 +1,77 @@
const specialNotesService = require("../services/special_notes");
const dateNotesService = require("../services/date_notes");
const eu = require("./etapi_utils");
const mappers = require("./mappers");
const getDateInvalidError = date => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
const getMonthInvalidError = month => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
const getYearInvalidError = year => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
function isValidDate(date) {
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date)) {
return false;
}
return !!Date.parse(date);
}
function register(router) {
eu.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
throw getDateInvalidError(res, date);
}
const note = specialNotesService.getInboxNote(date);
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'get', '/etapi/calendar/days/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
throw getDateInvalidError(res, date);
}
const note = dateNotesService.getDayNote(date);
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'get', '/etapi/calendar/weeks/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
throw getDateInvalidError(res, date);
}
const note = dateNotesService.getWeekNote(date);
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'get', '/etapi/calendar/months/:month', (req, res, next) => {
const {month} = req.params;
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
throw getMonthInvalidError(res, month);
}
const note = dateNotesService.getMonthNote(month);
res.json(mappers.mapNoteToPojo(note));
});
eu.route(router, 'get', '/etapi/calendar/years/:year', (req, res, next) => {
const {year} = req.params;
if (!/[0-9]{4}/.test(year)) {
throw getYearInvalidError(res, year);
}
const note = dateNotesService.getYearNote(year);
res.json(mappers.mapNoteToPojo(note));
});
}
module.exports = {
register
};

30
src/etapi/validators.js Normal file
View File

@ -0,0 +1,30 @@
function isString(obj) {
if (typeof obj !== 'string') {
return `'${obj}' is not a string`;
}
}
function isStringOrNull(obj) {
if (obj) {
return isString(obj);
}
}
function isBoolean(obj) {
if (typeof obj !== 'boolean') {
return `'${obj}' is not a boolean`;
}
}
function isInteger(obj) {
if (!Number.isInteger(obj)) {
return `'${obj}' is not an integer`;
}
}
module.exports = {
isString,
isStringOrNull,
isBoolean,
isInteger
};

View File

@ -5,7 +5,7 @@ import utils from "../services/utils.js";
const $dialog = $("#options-dialog"); const $dialog = $("#options-dialog");
export async function showDialog() { export async function showDialog(openTab) {
const options = await server.get('options'); const options = await server.get('options');
utils.openDialog($dialog); utils.openDialog($dialog);
@ -14,7 +14,8 @@ export async function showDialog() {
import('./options/appearance.js'), import('./options/appearance.js'),
import('./options/shortcuts.js'), import('./options/shortcuts.js'),
import('./options/code_notes.js'), import('./options/code_notes.js'),
import('./options/credentials.js'), import('./options/password.js'),
import('./options/etapi.js'),
import('./options/backup.js'), import('./options/backup.js'),
import('./options/sync.js'), import('./options/sync.js'),
import('./options/other.js'), import('./options/other.js'),
@ -26,4 +27,8 @@ export async function showDialog() {
tab.optionsLoaded(options) tab.optionsLoaded(options)
} }
}); });
if (openTab) {
$(`.nav-link[href='#options-${openTab}']`).trigger("click");
}
} }

View File

@ -0,0 +1,128 @@
import server from "../../services/server.js";
import utils from "../../services/utils.js";
const TPL = `
<h4>ETAPI</h4>
<p>ETAPI is a REST API used to access Trilium instance programmatically, without UI. <br/>
See more details on <a href="https://github.com/zadam/trilium/wiki/ETAPI">wiki</a> and <a onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">ETAPI OpenAPI spec</a>.</p>
<button type="button" class="btn btn-sm" id="create-etapi-token">Create new ETAPI token</button>
<br/><br/>
<h5>Existing tokens</h5>
<div id="no-tokens-yet">There are no tokens yet. Click on the button above to create one.</div>
<div style="overflow: auto; height: 500px;">
<table id="tokens-table" class="table table-stripped">
<thead>
<tr>
<th>Token name</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<style>
.token-table-button {
display: inline-block;
cursor: pointer;
padding: 3px;
margin-right: 20px;
font-size: large;
border: 1px solid transparent;
border-radius: 5px;
}
.token-table-button:hover {
border: 1px solid var(--main-border-color);
}
</style>
`;
export default class EtapiOptions {
constructor() {
$("#options-etapi").html(TPL);
$("#create-etapi-token").on("click", async () => {
const promptDialog = await import('../../dialogs/prompt.js');
const tokenName = await promptDialog.ask({
title: "New ETAPI token",
message: "Please enter new token's name",
defaultValue: "new token"
});
if (!tokenName.trim()) {
alert("Token name can't be empty");
return;
}
const {token} = await server.post('etapi-tokens', {tokenName});
await promptDialog.ask({
title: "ETAPI token created",
message: 'Copy the created token into clipboard. Trilium stores the token hashed and this is the last time you see it.',
defaultValue: token
});
this.refreshTokens();
});
this.refreshTokens();
}
async refreshTokens() {
const $noTokensYet = $("#no-tokens-yet");
const $tokensTable = $("#tokens-table");
const tokens = await server.get('etapi-tokens');
$noTokensYet.toggle(tokens.length === 0);
$tokensTable.toggle(tokens.length > 0);
const $tokensTableBody = $tokensTable.find("tbody");
$tokensTableBody.empty();
for (const token of tokens) {
$tokensTableBody.append(
$("<tr>")
.append($("<td>").text(token.name))
.append($("<td>").text(token.utcDateCreated))
.append($("<td>").append(
$('<span class="bx bx-pen token-table-button" title="Rename this token"></span>')
.on("click", () => this.renameToken(token.etapiTokenId, token.name)),
$('<span class="bx bx-trash token-table-button" title="Delete / deactive this token"></span>')
.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();
}
}

View File

@ -3,18 +3,16 @@ import protectedSessionHolder from "../../services/protected_session_holder.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
const TPL = ` const TPL = `
<h3>Username</h3> <h3 id="password-heading"></h3>
<p>Your username is <strong id="credentials-username"></strong>.</p>
<h3>Change password</h3>
<div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;"> <div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">
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, <a id="reset-password-button" href="javascript:">click here to reset it</a>.
</div> </div>
<form id="change-password-form"> <form id="change-password-form">
<div class="form-group"> <div class="form-group" id="old-password-form-group">
<label for="old-password">Old password</label> <label for="old-password">Old password</label>
<input class="form-control" id="old-password" type="password"> <input class="form-control" id="old-password" type="password">
</div> </div>
@ -29,24 +27,41 @@ const TPL = `
<input class="form-control" id="new-password2" type="password"> <input class="form-control" id="new-password2" type="password">
</div> </div>
<button class="btn btn-primary">Change password</button> <button class="btn btn-primary" id="save-password-button">Change password</button>
</form>`; </form>`;
export default class ChangePasswordOptions { export default class ChangePasswordOptions {
constructor() { constructor() {
$("#options-credentials").html(TPL); $("#options-password").html(TPL);
this.$username = $("#credentials-username"); this.$passwordHeading = $("#password-heading");
this.$form = $("#change-password-form"); this.$form = $("#change-password-form");
this.$oldPassword = $("#old-password"); this.$oldPassword = $("#old-password");
this.$newPassword1 = $("#new-password1"); this.$newPassword1 = $("#new-password1");
this.$newPassword2 = $("#new-password2"); 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()); this.$form.on('submit', () => this.save());
} }
optionsLoaded(options) { 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() { save() {

View File

@ -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' });
});
}

View File

@ -11,9 +11,11 @@ const $form = $("#prompt-dialog-form");
let resolve; let resolve;
let shownCb; let shownCb;
export function ask({ message, defaultValue, shown }) { export function ask({ title, message, defaultValue, shown }) {
shownCb = shown; shownCb = shown;
$("#prompt-title").text(title || "Prompt");
$question = $("<label>") $question = $("<label>")
.prop("for", "prompt-dialog-answer") .prop("for", "prompt-dialog-answer")
.text(message); .text(message);
@ -30,7 +32,7 @@ export function ask({ message, defaultValue, shown }) {
.append($question) .append($question)
.append($answer)); .append($answer));
utils.openDialog($dialog); utils.openDialog($dialog, false);
return new Promise((res, rej) => { resolve = res; }); return new Promise((res, rej) => { resolve = res; });
} }

View File

@ -12,7 +12,7 @@ export function show() {
} }
export function close() { 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 { try {
$dialog.modal('hide'); $dialog.modal('hide');
} }

View File

@ -118,7 +118,7 @@ class AppContext extends Component {
const appContext = new AppContext(window.glob.isMainWindow); const appContext = new AppContext(window.glob.isMainWindow);
// we should save all outstanding changes before the page/app is closed // we should save all outstanding changes before the page/app is closed
$(window).on('beforeunload', () => { $(window).on('beforeunload', () => {return "SSS";
let allSaved = true; let allSaved = true;
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref()); appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref());

View File

@ -81,6 +81,7 @@ async function renderAttributes(attributes, renderIsInheritable) {
const HIDDEN_ATTRIBUTES = [ const HIDDEN_ATTRIBUTES = [
'originalFileName', 'originalFileName',
'fileSize',
'template', 'template',
'cssClass', 'cssClass',
'iconClass', 'iconClass',

View File

@ -11,12 +11,12 @@ async function getInboxNote() {
/** @returns {NoteShort} */ /** @returns {NoteShort} */
async function getTodayNote() { async function getTodayNote() {
return await getDateNote(dayjs().format("YYYY-MM-DD")); return await getDayNote(dayjs().format("YYYY-MM-DD"));
} }
/** @returns {NoteShort} */ /** @returns {NoteShort} */
async function getDateNote(date) { async function getDayNote(date) {
const note = await server.get('special-notes/date/' + date, "date-note"); const note = await server.get('special-notes/days/' + date, "date-note");
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
@ -25,7 +25,7 @@ async function getDateNote(date) {
/** @returns {NoteShort} */ /** @returns {NoteShort} */
async function getWeekNote(date) { 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(); await ws.waitForMaxKnownEntityChangeId();
@ -34,7 +34,7 @@ async function getWeekNote(date) {
/** @returns {NoteShort} */ /** @returns {NoteShort} */
async function getMonthNote(month) { 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(); await ws.waitForMaxKnownEntityChangeId();
@ -43,7 +43,7 @@ async function getMonthNote(month) {
/** @returns {NoteShort} */ /** @returns {NoteShort} */
async function getYearNote(year) { 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(); await ws.waitForMaxKnownEntityChangeId();
@ -71,7 +71,7 @@ async function createSearchNote(opts = {}) {
export default { export default {
getInboxNote, getInboxNote,
getTodayNote, getTodayNote,
getDateNote, getDayNote,
getWeekNote, getWeekNote,
getMonthNote, getMonthNote,
getYearNote, getYearNote,

View File

@ -22,9 +22,9 @@ async function processEntityChanges(entityChanges) {
} else if (ec.entityName === 'note_contents') { } else if (ec.entityName === 'note_contents') {
delete froca.noteComplementPromises[ec.entityId]; delete froca.noteComplementPromises[ec.entityId];
loadResults.addNoteContent(ec.entityId, ec.sourceId); loadResults.addNoteContent(ec.entityId, ec.componentId);
} else if (ec.entityName === 'note_revisions') { } 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') { } else if (ec.entityName === 'note_revision_contents') {
// this should change only when toggling isProtected, ignore // this should change only when toggling isProtected, ignore
} else if (ec.entityName === 'options') { } else if (ec.entityName === 'options') {
@ -36,6 +36,9 @@ async function processEntityChanges(entityChanges) {
loadResults.addOption(ec.entity.name); loadResults.addOption(ec.entity.name);
} }
else if (ec.entityName === 'etapi_tokens') {
// NOOP
}
else { else {
throw new Error(`Unknown entityName ${ec.entityName}`); throw new Error(`Unknown entityName ${ec.entityName}`);
} }
@ -87,7 +90,7 @@ function processNoteChange(loadResults, ec) {
return; return;
} }
loadResults.addNote(ec.entityId, ec.sourceId); loadResults.addNote(ec.entityId, ec.componentId);
if (ec.isErased && ec.entityId in froca.notes) { if (ec.isErased && ec.entityId in froca.notes) {
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); 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]; delete parentNote.childToBranch[branch.noteId];
} }
loadResults.addBranch(ec.entityId, ec.sourceId); loadResults.addBranch(ec.entityId, ec.componentId);
delete froca.branches[ec.entityId]; delete froca.branches[ec.entityId];
} }
@ -133,7 +136,7 @@ function processBranchChange(loadResults, ec) {
return; return;
} }
loadResults.addBranch(ec.entityId, ec.sourceId); loadResults.addBranch(ec.entityId, ec.componentId);
const childNote = froca.notes[ec.entity.noteId]; const childNote = froca.notes[ec.entity.noteId];
const parentNote = froca.notes[ec.entity.parentNoteId]; 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) { function processAttributeChange(loadResults, ec) {
@ -199,7 +202,7 @@ function processAttributeChange(loadResults, ec) {
targetNote.targetRelations = targetNote.targetRelations.filter(attributeId => attributeId !== attribute.attributeId); 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]; delete froca.attributes[ec.entityId];
} }
@ -207,7 +210,7 @@ function processAttributeChange(loadResults, ec) {
return; return;
} }
loadResults.addAttribute(ec.entityId, ec.sourceId); loadResults.addAttribute(ec.entityId, ec.componentId);
const sourceNote = froca.notes[ec.entity.noteId]; const sourceNote = froca.notes[ec.entity.noteId];
const targetNote = ec.entity.type === 'relation' && froca.notes[ec.entity.value]; const targetNote = ec.entity.type === 'relation' && froca.notes[ec.entity.value];

View File

@ -389,16 +389,26 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
this.getTodayNote = dateNotesService.getTodayNote; 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<NoteShort>}
* @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 * @method
* @param {string} date - e.g. "2019-04-29" * @param {string} date - e.g. "2019-04-29"
* @return {Promise<NoteShort>} * @return {Promise<NoteShort>}
*/ */
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 * @method
* @param {string} date - e.g. "2019-04-29" * @param {string} date - e.g. "2019-04-29"

View File

@ -3,16 +3,17 @@ const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
const CODE_MIRROR = { const CODE_MIRROR = {
js: [ js: [
"libraries/codemirror/codemirror.js", "libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/mode/loadmode.js", "libraries/codemirror/addon/display/placeholder.js",
"libraries/codemirror/addon/mode/simple.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/edit/matchbrackets.js", "libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.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/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js", "libraries/codemirror/mode/meta.js",
"libraries/codemirror/keymap/vim.js", "libraries/codemirror/keymap/vim.js"
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
], ],
css: [ css: [
"libraries/codemirror/codemirror.css", "libraries/codemirror/codemirror.css",

View File

@ -9,8 +9,8 @@ export default class LoadResults {
} }
} }
this.noteIdToSourceId = {}; this.noteIdToComponentId = {};
this.sourceIdToNoteIds = {}; this.componentIdToNoteIds = {};
this.branches = []; this.branches = [];
@ -20,7 +20,7 @@ export default class LoadResults {
this.noteRevisions = []; this.noteRevisions = [];
this.contentNoteIdToSourceId = []; this.contentNoteIdToComponentId = [];
this.options = []; this.options = [];
} }
@ -29,22 +29,22 @@ export default class LoadResults {
return this.entities[entityName]?.[entityId]; return this.entities[entityName]?.[entityId];
} }
addNote(noteId, sourceId) { addNote(noteId, componentId) {
this.noteIdToSourceId[noteId] = this.noteIdToSourceId[noteId] || []; this.noteIdToComponentId[noteId] = this.noteIdToComponentId[noteId] || [];
if (!this.noteIdToSourceId[noteId].includes(sourceId)) { if (!this.noteIdToComponentId[noteId].includes(componentId)) {
this.noteIdToSourceId[noteId].push(sourceId); this.noteIdToComponentId[noteId].push(componentId);
} }
this.sourceIdToNoteIds[sourceId] = this.sourceIdToNoteIds[sourceId] || []; this.componentIdToNoteIds[componentId] = this.componentIdToNoteIds[componentId] || [];
if (!this.sourceIdToNoteIds[sourceId]) { if (!this.componentIdToNoteIds[componentId]) {
this.sourceIdToNoteIds[sourceId].push(noteId); this.componentIdToNoteIds[componentId].push(noteId);
} }
} }
addBranch(branchId, sourceId) { addBranch(branchId, componentId) {
this.branches.push({branchId, sourceId}); this.branches.push({branchId, componentId});
} }
getBranches() { getBranches() {
@ -53,7 +53,7 @@ export default class LoadResults {
.filter(branch => !!branch); .filter(branch => !!branch);
} }
addNoteReordering(parentNoteId, sourceId) { addNoteReordering(parentNoteId, componentId) {
this.noteReorderings.push(parentNoteId); this.noteReorderings.push(parentNoteId);
} }
@ -61,20 +61,20 @@ export default class LoadResults {
return this.noteReorderings; return this.noteReorderings;
} }
addAttribute(attributeId, sourceId) { addAttribute(attributeId, componentId) {
this.attributes.push({attributeId, sourceId}); this.attributes.push({attributeId, componentId});
} }
/** @returns {Attribute[]} */ /** @returns {Attribute[]} */
getAttributes(sourceId = 'none') { getAttributes(componentId = 'none') {
return this.attributes return this.attributes
.filter(row => row.sourceId !== sourceId) .filter(row => row.componentId !== componentId)
.map(row => this.getEntity("attributes", row.attributeId)) .map(row => this.getEntity("attributes", row.attributeId))
.filter(attr => !!attr); .filter(attr => !!attr);
} }
addNoteRevision(noteRevisionId, noteId, sourceId) { addNoteRevision(noteRevisionId, noteId, componentId) {
this.noteRevisions.push({noteRevisionId, noteId, sourceId}); this.noteRevisions.push({noteRevisionId, noteId, componentId});
} }
hasNoteRevisionForNote(noteId) { hasNoteRevisionForNote(noteId) {
@ -82,28 +82,28 @@ export default class LoadResults {
} }
getNoteIds() { getNoteIds() {
return Object.keys(this.noteIdToSourceId); return Object.keys(this.noteIdToComponentId);
} }
isNoteReloaded(noteId, sourceId = null) { isNoteReloaded(noteId, componentId = null) {
if (!noteId) { if (!noteId) {
return false; return false;
} }
const sourceIds = this.noteIdToSourceId[noteId]; const componentIds = this.noteIdToComponentId[noteId];
return sourceIds && !!sourceIds.find(sId => sId !== sourceId); return componentIds && !!componentIds.find(sId => sId !== componentId);
} }
addNoteContent(noteId, sourceId) { addNoteContent(noteId, componentId) {
this.contentNoteIdToSourceId.push({noteId, sourceId}); this.contentNoteIdToComponentId.push({noteId, componentId});
} }
isNoteContentReloaded(noteId, sourceId) { isNoteContentReloaded(noteId, componentId) {
if (!noteId) { if (!noteId) {
return false; 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) { addOption(name) {
@ -124,17 +124,17 @@ export default class LoadResults {
} }
isEmpty() { isEmpty() {
return Object.keys(this.noteIdToSourceId).length === 0 return Object.keys(this.noteIdToComponentId).length === 0
&& this.branches.length === 0 && this.branches.length === 0
&& this.attributes.length === 0 && this.attributes.length === 0
&& this.noteReorderings.length === 0 && this.noteReorderings.length === 0
&& this.noteRevisions.length === 0 && this.noteRevisions.length === 0
&& this.contentNoteIdToSourceId.length === 0 && this.contentNoteIdToComponentId.length === 0
&& this.options.length === 0; && this.options.length === 0;
} }
isEmptyForTree() { isEmptyForTree() {
return Object.keys(this.noteIdToSourceId).length === 0 return Object.keys(this.noteIdToComponentId).length === 0
&& this.branches.length === 0 && this.branches.length === 0
&& this.attributes.length === 0 && this.attributes.length === 0
&& this.noteReorderings.length === 0; && this.noteReorderings.length === 0;

View File

@ -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; export default NoteContext;

View File

@ -5,6 +5,7 @@ import ws from "./ws.js";
import appContext from "./app_context.js"; import appContext from "./app_context.js";
import froca from "./froca.js"; import froca from "./froca.js";
import utils from "./utils.js"; import utils from "./utils.js";
import options from "./options.js";
let protectedSessionDeferred = null; let protectedSessionDeferred = null;
@ -18,6 +19,11 @@ async function leaveProtectedSession() {
function enterProtectedSession() { function enterProtectedSession() {
const dfd = $.Deferred(); const dfd = $.Deferred();
if (!options.is("isPasswordSet")) {
import("../dialogs/password_not_set.js").then(dialog => dialog.show());
return dfd;
}
if (protectedSessionHolder.isProtectedSessionAvailable()) { if (protectedSessionHolder.isProtectedSessionAvailable()) {
dfd.resolve(false); dfd.resolve(false);
} }

View File

@ -53,8 +53,8 @@ export default class RootCommandExecutor extends Component {
d.showDialog(branchIds); d.showDialog(branchIds);
} }
showOptionsCommand() { showOptionsCommand({openTab}) {
import("../dialogs/options.js").then(d => d.showDialog()); import("../dialogs/options.js").then(d => d.showDialog(openTab));
} }
showHelpCommand() { showHelpCommand() {

View File

@ -9,7 +9,7 @@ async function getHeaders(headers) {
// headers need to be lowercase because node.js automatically converts them to lower case // 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 // also avoiding using underscores instead of dashes since nginx filters them out by default
const allHeaders = { const allHeaders = {
'trilium-source-id': glob.sourceId, 'trilium-component-id': glob.componentId,
'trilium-local-now-datetime': utils.localNowDateTime(), 'trilium-local-now-datetime': utils.localNowDateTime(),
'trilium-hoisted-note-id': activeNoteContext ? activeNoteContext.hoistedNoteId : null, 'trilium-hoisted-note-id': activeNoteContext ? activeNoteContext.hoistedNoteId : null,
'x-csrf-token': glob.csrfToken 'x-csrf-token': glob.csrfToken
@ -29,20 +29,24 @@ async function getHeaders(headers) {
return allHeaders; return allHeaders;
} }
async function get(url, sourceId) { async function get(url, componentId) {
return await call('GET', url, null, {'trilium-source-id': sourceId}); return await call('GET', url, null, {'trilium-component-id': componentId});
} }
async function post(url, data, sourceId) { async function post(url, data, componentId) {
return await call('POST', url, data, {'trilium-source-id': sourceId}); return await call('POST', url, data, {'trilium-component-id': componentId});
} }
async function put(url, data, sourceId) { async function put(url, data, componentId) {
return await call('PUT', url, data, {'trilium-source-id': sourceId}); return await call('PUT', url, data, {'trilium-component-id': componentId});
} }
async function remove(url, sourceId) { async function patch(url, data, componentId) {
return await call('DELETE', url, null, {'trilium-source-id': sourceId}); 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; let i = 1;
@ -185,6 +189,7 @@ export default {
get, get,
post, post,
put, put,
patch,
remove, remove,
ajax, ajax,
// don't remove, used from CKEditor image upload! // don't remove, used from CKEditor image upload!

View File

@ -245,10 +245,11 @@ function focusSavedElement() {
$lastFocusedElement = null; $lastFocusedElement = null;
} }
async function openDialog($dialog) { async function openDialog($dialog, closeActDialog = true) {
if (closeActDialog) {
closeActiveDialog(); closeActiveDialog();
glob.activeDialog = $dialog; glob.activeDialog = $dialog;
}
saveFocusedElement(); saveFocusedElement();

View File

@ -19,20 +19,23 @@ function SetupModel() {
this.setupSyncFromDesktop = ko.observable(false); this.setupSyncFromDesktop = ko.observable(false);
this.setupSyncFromServer = 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.syncServerHost = ko.observable();
this.syncProxy = ko.observable(); this.syncProxy = ko.observable();
this.password = ko.observable();
this.instanceType = utils.isElectron() ? "desktop" : "server";
this.setupTypeSelected = () => !!this.setupType(); this.setupTypeSelected = () => !!this.setupType();
this.selectSetupType = () => { this.selectSetupType = () => {
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.step(this.setupType());
}
}; };
this.back = () => { this.back = () => {
@ -42,54 +45,15 @@ function SetupModel() {
}; };
this.finish = async () => { this.finish = async () => {
if (this.setupType() === 'new-document') {
const username = this.username();
const password1 = this.password1();
const password2 = this.password2();
const theme = this.theme();
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");
});
}
else if (this.setupType() === 'sync-from-server') {
const syncServerHost = this.syncServerHost(); const syncServerHost = this.syncServerHost();
const syncProxy = this.syncProxy(); const syncProxy = this.syncProxy();
const username = this.username(); const password = this.password();
const password = this.password1();
if (!syncServerHost) { if (!syncServerHost) {
showAlert("Trilium server address can't be empty"); showAlert("Trilium server address can't be empty");
return; return;
} }
if (!username) {
showAlert("Username can't be empty");
return;
}
if (!password) { if (!password) {
showAlert("Password can't be empty"); showAlert("Password can't be empty");
return; return;
@ -99,7 +63,6 @@ function SetupModel() {
const resp = await $.post('api/setup/sync-from-server', { const resp = await $.post('api/setup/sync-from-server', {
syncServerHost: syncServerHost, syncServerHost: syncServerHost,
syncProxy: syncProxy, syncProxy: syncProxy,
username: username,
password: password password: password
}); });
@ -113,7 +76,6 @@ function SetupModel() {
else { else {
showAlert('Sync setup failed: ' + resp.error); showAlert('Sync setup failed: ' + resp.error);
} }
}
}; };
} }

View File

@ -55,7 +55,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
this.$dropdownContent.on('click', '.calendar-date', async ev => { this.$dropdownContent.on('click', '.calendar-date', async ev => {
const date = $(ev.target).closest('.calendar-date').attr('data-calendar-date'); 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) { if (note) {
appContext.tabManager.getActiveContext().setNote(note.noteId); appContext.tabManager.getActiveContext().setNote(note.noteId);

View File

@ -29,6 +29,10 @@ const TPL = `
font-family: var(--detail-font-family); font-family: var(--detail-font-family);
font-size: var(--detail-font-size); font-size: var(--detail-font-size);
} }
.note-detail.full-height {
height: 100%;
}
</style> </style>
</div> </div>
`; `;
@ -128,7 +132,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
await typeWidget.handleEvent('setNoteContext', {noteContext: this.noteContext}); 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', { await typeWidget.handleEvent('noteSwitched', {
noteContext: this.noteContext, noteContext: this.noteContext,
notePath: this.noteContext.notePath notePath: this.noteContext.notePath
@ -136,6 +140,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
this.child(typeWidget); 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() { getTypeWidget() {

View File

@ -5,8 +5,6 @@ const TPL = `
<div class="note-list-widget"> <div class="note-list-widget">
<style> <style>
.note-list-widget { .note-list-widget {
flex-grow: 100000;
flex-shrink: 100000;
min-height: 0; min-height: 0;
overflow: auto; overflow: auto;
} }
@ -22,11 +20,7 @@ const TPL = `
export default class NoteListWidget extends NoteContextAwareWidget { export default class NoteListWidget extends NoteContextAwareWidget {
isEnabled() { isEnabled() {
return super.isEnabled() return super.isEnabled() && this.noteContext.hasNoteList();
&& ['book', 'text', 'code'].includes(this.note.type)
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
&& this.note.hasChildren()
&& !this.note.hasLabel('hideChildrenOverview');
} }
doRender() { doRender() {

View File

@ -13,10 +13,12 @@ const TPL = `
<style> <style>
.note-detail-code { .note-detail-code {
position: relative; position: relative;
height: 100%;
} }
.note-detail-code-editor { .note-detail-code-editor {
min-height: 50px; min-height: 50px;
height: 100%;
} }
</style> </style>
@ -105,7 +107,8 @@ export default class EditableCodeTypeWidget extends TypeWidget {
// we linewrap partly also because without it horizontal scrollbar displays only when you scroll // 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 // all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem
lineWrapping: true, 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()); this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate());

View File

@ -36,6 +36,7 @@ const TPL = `
font-family: var(--detail-font-family); font-family: var(--detail-font-family);
padding-left: 14px; padding-left: 14px;
padding-top: 10px; padding-top: 10px;
height: 100%;
} }
.note-detail-editable-text a:hover { .note-detail-editable-text a:hover {
@ -73,6 +74,7 @@ const TPL = `
border: 0 !important; border: 0 !important;
box-shadow: none !important; box-shadow: none !important;
min-height: 50px; min-height: 50px;
height: 100%;
} }
</style> </style>

View File

@ -219,6 +219,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
else if (command === "editTitle") { else if (command === "editTitle") {
const promptDialog = await import("../../dialogs/prompt.js"); const promptDialog = await import("../../dialogs/prompt.js");
const title = await promptDialog.ask({ const title = await promptDialog.ask({
title: "Rename note",
message: "Enter new note title:", message: "Enter new note title:",
defaultValue: $title.text() defaultValue: $title.text()
}); });

View File

@ -241,6 +241,10 @@ body .CodeMirror {
background-color: #eeeeee background-color: #eeeeee
} }
.CodeMirror pre.CodeMirror-placeholder {
color: #999 !important;
}
#sql-console-query { #sql-console-query {
height: 150px; height: 150px;
width: 100%; width: 100%;

View File

@ -36,7 +36,7 @@ function getClipperInboxNote() {
let clipperInbox = attributeService.getNoteWithLabel('clipperInbox'); let clipperInbox = attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) { if (!clipperInbox) {
clipperInbox = dateNoteService.getDateNote(dateUtils.localNowDate()); clipperInbox = dateNoteService.getDayNote(dateUtils.localNowDate());
} }
return clipperInbox; return clipperInbox;

View File

@ -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
};

View File

@ -3,16 +3,15 @@
const options = require('../../services/options'); const options = require('../../services/options');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const dateUtils = require('../../services/date_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 passwordEncryptionService = require('../../services/password_encryption');
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info'); const appInfo = require('../../services/app_info');
const eventService = require('../../services/events'); const eventService = require('../../services/events');
const sqlInit = require('../../services/sql_init'); const sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const optionService = require('../../services/options');
const ApiToken = require('../../becca/entities/api_token');
const ws = require("../../services/ws"); const ws = require("../../services/ws");
const etapiTokenService = require("../../services/etapi_tokens");
function loginSync(req) { function loginSync(req) {
if (!sqlInit.schemaExists()) { if (!sqlInit.schemaExists()) {
@ -48,7 +47,7 @@ function loginSync(req) {
req.session.loggedIn = true; req.session.loggedIn = true;
return { return {
sourceId: sourceIdService.getCurrentSourceId(), instanceId: instanceId,
maxEntityChangeId: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1") maxEntityChangeId: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1")
}; };
} }
@ -85,23 +84,18 @@ function logoutFromProtectedSession() {
} }
function token(req) { function token(req) {
const username = req.body.username;
const password = req.body.password; const password = req.body.password;
const isUsernameValid = username === optionService.getOption('username'); if (!passwordEncryptionService.verifyPassword(password)) {
const isPasswordValid = passwordEncryptionService.verifyPassword(password); return [401, "Incorrect password"];
if (!isUsernameValid || !isPasswordValid) {
return [401, "Incorrect username/password"];
} }
const apiToken = new ApiToken({ // for backwards compatibility with Sender which does not send the name
token: utils.randomSecureToken() const tokenName = req.body.tokenName || "Trilium Sender / Web Clipper";
}).save();
return { const {authToken} = etapiTokenService.createToken(tokenName);
token: apiToken.token
}; return { token: authToken };
} }
module.exports = { module.exports = {

View File

@ -73,9 +73,7 @@ function deleteNote(req) {
const taskContext = TaskContext.getInstance(taskId, 'delete-notes'); const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
for (const branch of note.getParentBranches()) { noteService.deleteNote(note, deleteId, taskContext);
noteService.deleteBranch(branch, deleteId, taskContext);
}
if (eraseNotes) { if (eraseNotes) {
noteService.eraseNotesWithDeleteId(deleteId); noteService.eraseNotesWithDeleteId(deleteId);

View File

@ -6,7 +6,6 @@ const searchService = require('../../services/search/services/search');
// options allowed to be updated directly in options dialog // options allowed to be updated directly in options dialog
const ALLOWED_OPTIONS = new Set([ const ALLOWED_OPTIONS = new Set([
'username', // not exposed for update (not harmful anyway), needed for reading
'eraseEntitiesAfterTimeInSeconds', 'eraseEntitiesAfterTimeInSeconds',
'protectedSessionTimeout', 'protectedSessionTimeout',
'noteRevisionSnapshotTimeInterval', 'noteRevisionSnapshotTimeInterval',
@ -69,6 +68,8 @@ function getOptions() {
} }
} }
resultMap['isPasswordSet'] = !!optionMap['passwordVerificationHash'] ? 'true' : 'false';
return resultMap; return resultMap;
} }

View File

@ -1,11 +1,26 @@
"use strict"; "use strict";
const changePasswordService = require('../../services/change_password'); const passwordService = require('../../services/password');
function changePassword(req) { 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 = { module.exports = {
changePassword changePassword,
resetPassword
}; };

View File

@ -15,7 +15,7 @@ function uploadImage(req) {
const originalName = "Sender image." + imageType(file.buffer).ext; 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); const {note, noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
@ -35,7 +35,7 @@ function uploadImage(req) {
} }
function saveNote(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({ const {note, branch} = noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,

View File

@ -13,16 +13,14 @@ function getStatus() {
}; };
} }
async function setupNewDocument(req) { async function setupNewDocument() {
const { username, password, theme } = req.body; await sqlInit.createInitialDatabase();
await sqlInit.createInitialDatabase(username, password, theme);
} }
function setupSyncFromServer(req) { 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) { function saveSyncSeed(req) {

View File

@ -10,8 +10,8 @@ function getInboxNote(req) {
return specialNotesService.getInboxNote(req.params.date); return specialNotesService.getInboxNote(req.params.date);
} }
function getDateNote(req) { function getDayNote(req) {
return dateNoteService.getDateNote(req.params.date); return dateNoteService.getDayNote(req.params.date);
} }
function getWeekNote(req) { function getWeekNote(req) {
@ -26,7 +26,7 @@ function getYearNote(req) {
return dateNoteService.getYearNote(req.params.year); return dateNoteService.getYearNote(req.params.year);
} }
function getDateNotesForMonth(req) { function getDayNotesForMonth(req) {
const month = req.params.month; const month = req.params.month;
return sql.getMap(` return sql.getMap(`
@ -68,11 +68,11 @@ function getHoistedNote() {
module.exports = { module.exports = {
getInboxNote, getInboxNote,
getDateNote, getDayNote,
getWeekNote, getWeekNote,
getMonthNote, getMonthNote,
getYearNote, getYearNote,
getDateNotesForMonth, getDayNotesForMonth,
createSqlConsole, createSqlConsole,
saveSqlConsole, saveSqlConsole,
createSearchNote, createSearchNote,

View File

@ -123,13 +123,45 @@ function forceNoteSync(req) {
function getChanged(req) { function getChanged(req) {
const startTime = Date.now(); 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 = { const ret = {
entityChanges: syncService.getEntityChangesRecords(entityChanges), entityChanges: entityChangeRecords,
maxEntityChangeId: sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1') lastEntityChangeId,
outstandingPullCount: sql.getValue(`
SELECT COUNT(id)
FROM entity_changes
WHERE isSynced = 1
AND instanceId != ?
AND id > ?`, [clientinstanceId, lastEntityChangeId])
}; };
if (ret.entityChanges.length > 0) { 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) { for (const {entityChange, entity} of entities) {
syncUpdateService.updateEntity(entityChange, entity); syncUpdateService.updateEntity(entityChange, entity, instanceId);
} }
} }

View File

@ -1,6 +1,5 @@
"use strict"; "use strict";
const sourceIdService = require('../services/source_id');
const sql = require('../services/sql'); const sql = require('../services/sql');
const attributeService = require('../services/attributes'); const attributeService = require('../services/attributes');
const config = require('../services/config'); const config = require('../services/config');
@ -28,7 +27,6 @@ function index(req, res) {
mainFontSize: parseInt(options.mainFontSize), mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize), treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize), detailFontSize: parseInt(options.detailFontSize),
sourceId: sourceIdService.generateSourceId(),
maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"), 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"), maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"),
instanceName: config.General ? config.General.instanceName : null, instanceName: config.General ? config.General.instanceName : null,

View File

@ -4,17 +4,47 @@ const utils = require('../services/utils');
const optionService = require('../services/options'); const optionService = require('../services/options');
const myScryptService = require('../services/my_scrypt'); const myScryptService = require('../services/my_scrypt');
const log = require('../services/log'); const log = require('../services/log');
const passwordService = require("../services/password");
function loginPage(req, res) { function loginPage(req, res) {
res.render('login', { failedAuth: false }); res.render('login', { failedAuth: false });
} }
function login(req, res) { function setPasswordPage(req, res) {
const userName = optionService.getOption('username'); 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; const guessedPassword = req.body.password;
if (req.body.username === userName && verifyPassword(guessedPassword)) { if (verifyPassword(guessedPassword)) {
const rememberMe = req.body.remember_me; const rememberMe = req.body.remember_me;
req.session.regenerate(() => { req.session.regenerate(() => {
@ -30,7 +60,7 @@ function login(req, res) {
} }
else { else {
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy // 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}); res.render('login', {'failedAuth': true});
} }
@ -55,6 +85,8 @@ function logout(req, res) {
module.exports = { module.exports = {
loginPage, loginPage,
setPasswordPage,
setPassword,
login, login,
logout logout
}; };

View File

@ -31,15 +31,22 @@ const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender'); const senderRoute = require('./api/sender');
const filesRoute = require('./api/files'); const filesRoute = require('./api/files');
const searchRoute = require('./api/search'); const searchRoute = require('./api/search');
const specialNotesRoute = require('./api/special_notes.js'); const specialNotesRoute = require('./api/special_notes');
const noteMapRoute = require('./api/note_map.js'); const noteMapRoute = require('./api/note_map');
const clipperRoute = require('./api/clipper'); const clipperRoute = require('./api/clipper');
const similarNotesRoute = require('./api/similar_notes'); const similarNotesRoute = require('./api/similar_notes');
const keysRoute = require('./api/keys'); const keysRoute = require('./api/keys');
const backendLogRoute = require('./api/backend_log'); const backendLogRoute = require('./api/backend_log');
const statsRoute = require('./api/stats'); const statsRoute = require('./api/stats');
const fontsRoute = require('./api/fonts'); const fontsRoute = require('./api/fonts');
const etapiTokensApiRoutes = require('./api/etapi_tokens');
const shareRoutes = require('../share/routes'); 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 log = require('../services/log');
const express = require('express'); const express = require('express');
@ -51,7 +58,7 @@ const entityChangesService = require('../services/entity_changes');
const csurf = require('csurf'); const csurf = require('csurf');
const {createPartialContentHandler} = require("express-partial-content"); const {createPartialContentHandler} = require("express-partial-content");
const rateLimit = require("express-rate-limit"); const rateLimit = require("express-rate-limit");
const AbstractEntity = require("../becca/entities/abstract_entity.js"); const AbstractEntity = require("../becca/entities/abstract_entity");
const csrfMiddleware = csurf({ const csrfMiddleware = csurf({
cookie: true, cookie: true,
@ -139,7 +146,7 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
cls.namespace.bindEmitter(res); cls.namespace.bindEmitter(res);
const result = cls.init(() => { 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('localNowDateTime', req.headers['trilium-local-now-datetime']);
cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root'); 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'); const uploadMiddleware = multer.single('upload');
function register(app) { function register(app) {
route(GET, '/', [auth.checkAuth, csrfMiddleware], indexRoute.index); 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({ const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000, // 15 minutes
@ -191,6 +199,7 @@ function register(app) {
route(POST, '/login', [loginRateLimiter], loginRoute.login); route(POST, '/login', [loginRateLimiter], loginRoute.login);
route(POST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout); route(POST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout);
route(POST, '/set-password', [auth.checkAppInitialized], loginRoute.setPassword);
route(GET, '/setup', [], setupRoute.setupPage); route(GET, '/setup', [], setupRoute.setupPage);
apiRoute(GET, '/api/tree', treeApiRoute.getTree); 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/note-map/:noteId/backlinks', noteMapRoute.getBacklinks);
apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote);
apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote); apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote);
apiRoute(GET, '/api/special-notes/week/:date', specialNotesRoute.getWeekNote); apiRoute(GET, '/api/special-notes/weeks/:date', specialNotesRoute.getWeekNote);
apiRoute(GET, '/api/special-notes/month/:month', specialNotesRoute.getMonthNote); apiRoute(GET, '/api/special-notes/months/:month', specialNotesRoute.getMonthNote);
apiRoute(GET, '/api/special-notes/year/:year', specialNotesRoute.getYearNote); apiRoute(GET, '/api/special-notes/years/:year', specialNotesRoute.getYearNote);
apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDateNotesForMonth); 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/sql-console', specialNotesRoute.createSqlConsole);
apiRoute(POST, '/api/special-notes/save-sql-console', specialNotesRoute.saveSqlConsole); apiRoute(POST, '/api/special-notes/save-sql-console', specialNotesRoute.saveSqlConsole);
apiRoute(POST, '/api/special-notes/search-note', specialNotesRoute.createSearchNote); 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(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes);
apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword); 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/test', syncApiRoute.testSync);
apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow); apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow);
@ -333,8 +343,8 @@ function register(app) {
// no CSRF since this is called from android app // no CSRF since this is called from android app
route(POST, '/api/sender/login', [], loginApiRoute.token, apiResultHandler); route(POST, '/api/sender/login', [], loginApiRoute.token, apiResultHandler);
route(POST, '/api/sender/image', [auth.checkToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler); route(POST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler); route(POST, '/api/sender/note', [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler);
apiRoute(GET, '/api/quick-search/:searchString', searchRoute.quickSearch); apiRoute(GET, '/api/quick-search/:searchString', searchRoute.quickSearch);
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
@ -350,7 +360,7 @@ function register(app) {
route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler); route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler);
// in case of local electron, local calls are allowed unauthenticated, for server they need auth // 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(GET, '/api/clipper/handshake', clipperMiddleware, clipperRoute.handshake, apiResultHandler);
route(POST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler); route(POST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler);
@ -371,8 +381,20 @@ function register(app) {
route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss); 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); shareRoutes.register(router);
etapiAuthRoutes.register(router);
etapiAttributeRoutes.register(router);
etapiBranchRoutes.register(router);
etapiNoteRoutes.register(router);
etapiSpecialNoteRoutes.register(router);
etapiSpecRoute.register(router);
app.use('', router); app.use('', router);
} }

View File

@ -4,8 +4,8 @@ const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir'); const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 189; const APP_DB_VERSION = 194;
const SYNC_VERSION = 23; const SYNC_VERSION = 25;
const CLIPPER_PROTOCOL_VERSION = "1.0"; const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = { module.exports = {

View File

@ -1,12 +1,12 @@
"use strict"; "use strict";
const sql = require('./sql'); const etapiTokenService = require("./etapi_tokens");
const log = require('./log'); const log = require('./log');
const sqlInit = require('./sql_init'); const sqlInit = require('./sql_init');
const utils = require('./utils'); const utils = require('./utils');
const passwordEncryptionService = require('./password_encryption'); const passwordEncryptionService = require('./password_encryption');
const optionService = require('./options');
const config = require('./config'); const config = require('./config');
const passwordService = require("./password");
const noAuthentication = config.General && config.General.noAuthentication === true; const noAuthentication = config.General && config.General.noAuthentication === true;
@ -15,7 +15,11 @@ function checkAuth(req, res, next) {
res.redirect("setup"); res.redirect("setup");
} }
else if (!req.session.loggedIn && !utils.isElectron() && !noAuthentication) { else if (!req.session.loggedIn && !utils.isElectron() && !noAuthentication) {
if (passwordService.isPasswordSet()) {
res.redirect("login"); res.redirect("login");
} else {
res.redirect("set-password");
}
} }
else { else {
next(); 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) { function checkAppNotInitialized(req, res, next) {
if (sqlInit.isDbInitialized()) { if (sqlInit.isDbInitialized()) {
reject(req, res, "App already initialized."); reject(req, res, "App already initialized.");
@ -60,15 +72,12 @@ function checkAppNotInitialized(req, res, next) {
} }
} }
function checkToken(req, res, next) { function checkEtapiToken(req, res, next) {
const token = req.headers.authorization; if (etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
next();
// 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");
} }
else { 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 auth = new Buffer.from(header, 'base64').toString();
const [username, password] = auth.split(/:/); const [username, password] = auth.split(/:/);
const dbUsername = optionService.getOption('username'); // username is ignored
if (dbUsername !== username || !passwordEncryptionService.verifyPassword(password)) { if (!passwordEncryptionService.verifyPassword(password)) {
res.status(401).send('Incorrect username and/or password'); res.status(401).send('Incorrect password');
} }
else { else {
next(); next();
@ -101,8 +110,9 @@ module.exports = {
checkAuth, checkAuth,
checkApiAuth, checkApiAuth,
checkAppInitialized, checkAppInitialized,
checkPasswordSet,
checkAppNotInitialized, checkAppNotInitialized,
checkApiAuthOrElectron, checkApiAuthOrElectron,
checkToken, checkEtapiToken,
checkCredentials checkCredentials
}; };

View File

@ -134,18 +134,18 @@ function BackendScriptApi(currentNote, apiParams) {
this.getNoteWithLabel = attributeService.getNoteWithLabel; 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 * @method
* @param {string} noteId * @param {string} noteId
* @param {string} parentNoteId * @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} * @returns {void}
*/ */
this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent; 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 * @method
* @param {string} noteId * @param {string} noteId
@ -309,8 +309,18 @@ function BackendScriptApi(currentNote, apiParams) {
* @method * @method
* @param {string} date in YYYY-MM-DD format * @param {string} date in YYYY-MM-DD format
* @returns {Note|null} * @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. * Returns today's day note. If such note doesn't exist, it is created.

View File

@ -76,7 +76,7 @@ async function anonymize() {
const db = new Database(anonymizedFile); 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 notes SET title = 'title'").run();
db.prepare("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL").run(); db.prepare("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL").run();
db.prepare("UPDATE note_revisions SET title = 'title'").run(); db.prepare("UPDATE note_revisions SET title = 'title'").run();

View File

@ -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
};

View File

@ -28,8 +28,8 @@ function getHoistedNoteId() {
return namespace.get('hoistedNoteId') || 'root'; return namespace.get('hoistedNoteId') || 'root';
} }
function getSourceId() { function getComponentId() {
return namespace.get('sourceId'); return namespace.get('componentId');
} }
function getLocalNowDateTime() { function getLocalNowDateTime() {
@ -80,7 +80,7 @@ module.exports = {
set, set,
namespace, namespace,
getHoistedNoteId, getHoistedNoteId,
getSourceId, getComponentId,
getLocalNowDateTime, getLocalNowDateTime,
disableEntityEvents, disableEntityEvents,
isEntityEventsDisabled, isEntityEventsDisabled,

View File

@ -567,7 +567,7 @@ class ConsistencyChecks {
this.runEntityChangeChecks("note_revisions", "noteRevisionId"); this.runEntityChangeChecks("note_revisions", "noteRevisionId");
this.runEntityChangeChecks("branches", "branchId"); this.runEntityChangeChecks("branches", "branchId");
this.runEntityChangeChecks("attributes", "attributeId"); this.runEntityChangeChecks("attributes", "attributeId");
this.runEntityChangeChecks("api_tokens", "apiTokenId"); this.runEntityChangeChecks("etapi_tokens", "etapiTokenId");
this.runEntityChangeChecks("options", "name"); this.runEntityChangeChecks("options", "name");
} }
@ -660,7 +660,7 @@ class ConsistencyChecks {
return `${tableName}: ${count}`; 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(", ")); log.info("Table counts: " + tables.map(tableName => getTableRowCount(tableName)).join(", "));
} }

View File

@ -3,7 +3,6 @@
const noteService = require('./notes'); const noteService = require('./notes');
const attributeService = require('./attributes'); const attributeService = require('./attributes');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const becca = require('../becca/becca');
const sql = require('./sql'); const sql = require('./sql');
const protectedSessionService = require('./protected_session'); const protectedSessionService = require('./protected_session');
@ -49,7 +48,7 @@ function getRootCalendarNote() {
} }
/** @returns {Note} */ /** @returns {Note} */
function getYearNote(dateStr, rootNote) { function getYearNote(dateStr, rootNote = null) {
if (!rootNote) { if (!rootNote) {
rootNote = getRootCalendarNote(); rootNote = getRootCalendarNote();
} }
@ -88,7 +87,7 @@ function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
} }
/** @returns {Note} */ /** @returns {Note} */
function getMonthNote(dateStr, rootNote) { function getMonthNote(dateStr, rootNote = null) {
if (!rootNote) { if (!rootNote) {
rootNote = getRootCalendarNote(); rootNote = getRootCalendarNote();
} }
@ -124,7 +123,7 @@ function getMonthNote(dateStr, rootNote) {
return monthNote; return monthNote;
} }
function getDateNoteTitle(rootNote, dayNumber, dateObj) { function getDayNoteTitle(rootNote, dayNumber, dateObj) {
const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}"; const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}";
const weekDay = DAYS[dateObj.getDay()]; const weekDay = DAYS[dateObj.getDay()];
@ -137,7 +136,7 @@ function getDateNoteTitle(rootNote, dayNumber, dateObj) {
} }
/** @returns {Note} */ /** @returns {Note} */
function getDateNote(dateStr) { function getDayNote(dateStr) {
dateStr = dateStr.trim().substr(0, 10); dateStr = dateStr.trim().substr(0, 10);
let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr); let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
@ -152,7 +151,7 @@ function getDateNote(dateStr) {
const dateObj = dateUtils.parseLocalDate(dateStr); const dateObj = dateUtils.parseLocalDate(dateStr);
const noteTitle = getDateNoteTitle(rootNote, dayNumber, dateObj); const noteTitle = getDayNoteTitle(rootNote, dayNumber, dateObj);
sql.transactional(() => { sql.transactional(() => {
dateNote = createNote(monthNote, noteTitle); dateNote = createNote(monthNote, noteTitle);
@ -170,7 +169,7 @@ function getDateNote(dateStr) {
} }
function getTodayNote() { function getTodayNote() {
return getDateNote(dateUtils.localNowDate()); return getDayNote(dateUtils.localNowDate());
} }
function getStartOfTheWeek(date, startOfTheWeek) { function getStartOfTheWeek(date, startOfTheWeek) {
@ -197,7 +196,7 @@ function getWeekNote(dateStr, options = {}) {
dateStr = dateUtils.utcDateTimeStr(dateObj); dateStr = dateUtils.utcDateTimeStr(dateObj);
return getDateNote(dateStr); return getDayNote(dateStr);
} }
module.exports = { module.exports = {
@ -205,6 +204,6 @@ module.exports = {
getYearNote, getYearNote,
getMonthNote, getMonthNote,
getWeekNote, getWeekNote,
getDateNote, getDayNote,
getTodayNote getTodayNote
}; };

View File

@ -1,13 +1,19 @@
const sql = require('./sql'); const sql = require('./sql');
const sourceIdService = require('./source_id');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const log = require('./log'); const log = require('./log');
const cls = require('./cls'); const cls = require('./cls');
const utils = require('./utils'); const utils = require('./utils');
const instanceId = require('./member_id');
const becca = require("../becca/becca"); const becca = require("../becca/becca");
let maxEntityChangeId = 0; let maxEntityChangeId = 0;
function addEntityChangeWithinstanceId(origEntityChange, instanceId) {
const ec = {...origEntityChange, instanceId};
return addEntityChange(ec);
}
function addEntityChange(origEntityChange) { function addEntityChange(origEntityChange) {
const ec = {...origEntityChange}; const ec = {...origEntityChange};
@ -17,7 +23,8 @@ function addEntityChange(origEntityChange) {
ec.changeId = utils.randomString(12); 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.isSynced = ec.isSynced ? 1 : 0;
ec.isErased = ec.isErased ? 1 : 0; ec.isErased = ec.isErased ? 1 : 0;
ec.id = sql.replace("entity_changes", ec); ec.id = sql.replace("entity_changes", ec);
@ -27,7 +34,7 @@ function addEntityChange(origEntityChange) {
cls.addEntityChange(ec); cls.addEntityChange(ec);
} }
function addNoteReorderingEntityChange(parentNoteId, sourceId) { function addNoteReorderingEntityChange(parentNoteId, componentId) {
addEntityChange({ addEntityChange({
entityName: "note_reordering", entityName: "note_reordering",
entityId: parentNoteId, entityId: parentNoteId,
@ -35,7 +42,8 @@ function addNoteReorderingEntityChange(parentNoteId, sourceId) {
isErased: false, isErased: false,
utcDateChanged: dateUtils.utcNowDateTime(), utcDateChanged: dateUtils.utcNowDateTime(),
isSynced: true, isSynced: true,
sourceId componentId,
instanceId: instanceId
}); });
const eventService = require('./events'); const eventService = require('./events');
@ -129,7 +137,7 @@ function fillAllEntityChanges() {
fillEntityChanges("note_revision_contents", "noteRevisionId"); fillEntityChanges("note_revision_contents", "noteRevisionId");
fillEntityChanges("recent_notes", "noteId"); fillEntityChanges("recent_notes", "noteId");
fillEntityChanges("attributes", "attributeId"); fillEntityChanges("attributes", "attributeId");
fillEntityChanges("api_tokens", "apiTokenId"); fillEntityChanges("etapi_tokens", "etapiTokenId");
fillEntityChanges("options", "name", 'isSynced = 1'); fillEntityChanges("options", "name", 'isSynced = 1');
}); });
} }
@ -138,6 +146,7 @@ module.exports = {
addNoteReorderingEntityChange, addNoteReorderingEntityChange,
moveEntityChangeToTop, moveEntityChangeToTop,
addEntityChange, addEntityChange,
addEntityChangeWithinstanceId,
fillAllEntityChanges, fillAllEntityChanges,
addEntityChangesForSector, addEntityChangesForSector,
getMaxEntityChangeId: () => maxEntityChangeId getMaxEntityChangeId: () => maxEntityChangeId

View File

@ -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
};

View File

@ -0,0 +1,5 @@
const utils = require('./utils');
const instanceId = utils.randomString(12);
module.exports = instanceId;

View File

@ -18,6 +18,8 @@ const Branch = require('../becca/entities/branch');
const Note = require('../becca/entities/note'); const Note = require('../becca/entities/note');
const Attribute = require('../becca/entities/attribute'); const Attribute = require('../becca/entities/attribute');
// TODO: patch/put note content
function getNewNotePosition(parentNoteId) { function getNewNotePosition(parentNoteId) {
const note = becca.notes[parentNoteId]; const note = becca.notes[parentNoteId];
@ -107,6 +109,10 @@ function createNewNote(params) {
throw new Error(`Note title must be set`); 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(() => { return sql.transactional(() => {
const note = new Note({ const note = new Note({
noteId: params.noteId, // optionally can force specific noteId noteId: params.noteId, // optionally can force specific noteId
@ -520,7 +526,7 @@ function updateNote(noteId, noteUpdates) {
/** /**
* @param {Branch} branch * @param {Branch} branch
* @param {string} deleteId * @param {string|null} deleteId
* @param {TaskContext} taskContext * @param {TaskContext} taskContext
* *
* @return {boolean} - true if note has been deleted, false otherwise * @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 {string} noteId
* @param {TaskContext} taskContext * @param {TaskContext} taskContext
@ -915,6 +932,7 @@ module.exports = {
createNewNoteWithTarget, createNewNoteWithTarget,
updateNote, updateNote,
deleteBranch, deleteBranch,
deleteNote,
undeleteNote, undeleteNote,
protectNoteRecursively, protectNoteRecursively,
scanForLinks, scanForLinks,

View File

@ -1,5 +1,5 @@
const becca = require('../becca/becca'); const becca = require('../becca/becca');
const sql = require("./sql.js"); const sql = require("./sql");
function getOption(name) { function getOption(name) {
let option; let option;

View File

@ -1,6 +1,4 @@
const optionService = require('./options'); const optionService = require('./options');
const passwordEncryptionService = require('./password_encryption');
const myScryptService = require('./my_scrypt');
const appInfo = require('./app_info'); const appInfo = require('./app_info');
const utils = require('./utils'); const utils = require('./utils');
const log = require('./log'); const log = require('./log');
@ -12,21 +10,6 @@ function initDocumentOptions() {
optionService.createOption('documentSecret', utils.randomSecureToken(16), false); 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 = {}) { function initNotSyncedOptions(initialized, opts = {}) {
optionService.createOption('openTabs', JSON.stringify([ optionService.createOption('openTabs', JSON.stringify([
{ {
@ -45,7 +28,15 @@ function initNotSyncedOptions(initialized, opts = {}) {
optionService.createOption('lastSyncedPull', '0', false); optionService.createOption('lastSyncedPull', '0', false);
optionService.createOption('lastSyncedPush', '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('syncServerHost', opts.syncServerHost || '', false);
optionService.createOption('syncServerTimeout', '120000', false); optionService.createOption('syncServerTimeout', '120000', false);
@ -130,7 +121,6 @@ function getKeyboardDefaultOptions() {
module.exports = { module.exports = {
initDocumentOptions, initDocumentOptions,
initSyncedOptions,
initNotSyncedOptions, initNotSyncedOptions,
initStartupOptions initStartupOptions
}; };

83
src/services/password.js Normal file
View File

@ -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
};

View File

@ -38,7 +38,7 @@ function exec(opts) {
}; };
if (opts.auth) { 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({ const request = client.request({

View File

@ -30,9 +30,9 @@ function executeBundle(bundle, apiParams = {}) {
apiParams.startNote = bundle.note; 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 // last \r\n is necessary if script contains line comment on its last line
const script = "function() {\r\n" + bundle.script + "\r\n}"; const script = "function() {\r\n" + bundle.script + "\r\n}";
@ -47,7 +47,7 @@ function executeBundle(bundle, apiParams = {}) {
throw e; throw e;
} }
finally { finally {
cls.set('sourceId', originalSourceId); cls.set('componentId', originalComponentId);
} }
} }

View File

@ -18,6 +18,7 @@ class SearchContext {
this.orderDirection = params.orderDirection; this.orderDirection = params.orderDirection;
this.limit = params.limit; this.limit = params.limit;
this.debug = params.debug; this.debug = params.debug;
this.debugInfo = null;
this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch; this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch;
this.highlightedTokens = []; this.highlightedTokens = [];
this.originalQuery = ""; this.originalQuery = "";

View File

@ -11,7 +11,7 @@ const RelationWhereExp = require('../expressions/relation_where');
const PropertyComparisonExp = require('../expressions/property_comparison'); const PropertyComparisonExp = require('../expressions/property_comparison');
const AttributeExistsExp = require('../expressions/attribute_exists'); const AttributeExistsExp = require('../expressions/attribute_exists');
const LabelComparisonExp = require('../expressions/label_comparison'); 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 NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext');
const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext'); const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit'); const OrderByAndLimitExp = require('../expressions/order_by_and_limit');

Some files were not shown because too many files have changed in this diff Show More