mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
Merge remote-tracking branch 'origin/next50'
This commit is contained in:
commit
916ff5f2ee
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -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
3
TODO
@ -1,3 +0,0 @@
|
|||||||
- new icon
|
|
||||||
- polish becca entities API
|
|
||||||
- separate private and public APIs in becca entities
|
|
1
db/migrations/0189__delete_username_option.sql
Normal file
1
db/migrations/0189__delete_username_option.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DELETE FROM options WHERE name = 'username';
|
13
db/migrations/0190__change_to_etapi_tokens.sql
Normal file
13
db/migrations/0190__change_to_etapi_tokens.sql
Normal 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;
|
10
db/migrations/0191__hash_tokens.js
Normal file
10
db/migrations/0191__hash_tokens.js
Normal 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]);
|
||||||
|
}
|
||||||
|
};
|
24
db/migrations/0192__add_memberId_to_entity_changes.sql
Normal file
24
db/migrations/0192__add_memberId_to_entity_changes.sql
Normal 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`
|
||||||
|
);
|
1
db/migrations/0193__add_index_to_changeId.sql
Normal file
1
db/migrations/0193__add_index_to_changeId.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX `IDX_entity_changes_changeId` ON `entity_changes` (`changeId`);
|
@ -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
|
||||||
|
78
libraries/codemirror/addon/display/placeholder.js
vendored
Normal file
78
libraries/codemirror/addon/display/placeholder.js
vendored
Normal 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
11479
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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", () => {
|
||||||
|
@ -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} */
|
||||||
|
@ -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", () => {
|
||||||
|
@ -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 => {
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
|
@ -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");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
72
src/becca/entities/etapi_token.js
Normal file
72
src/becca/entities/etapi_token.js
Normal 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;
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
64
src/etapi/attributes.js
Normal 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
43
src/etapi/auth.js
Normal 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
80
src/etapi/branches.js
Normal 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
|
||||||
|
};
|
789
src/etapi/etapi.openapi.yaml
Normal file
789
src/etapi/etapi.openapi.yaml
Normal 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
139
src/etapi/etapi_utils.js
Normal 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
49
src/etapi/mappers.js
Normal 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
181
src/etapi/notes.js
Normal 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
20
src/etapi/spec.js
Normal 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
|
||||||
|
};
|
77
src/etapi/special_notes.js
Normal file
77
src/etapi/special_notes.js
Normal 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
30
src/etapi/validators.js
Normal 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
|
||||||
|
};
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
128
src/public/app/dialogs/options/etapi.js
Normal file
128
src/public/app/dialogs/options/etapi.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
13
src/public/app/dialogs/password_not_set.js
Normal file
13
src/public/app/dialogs/password_not_set.js
Normal 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' });
|
||||||
|
});
|
||||||
|
}
|
@ -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; });
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -81,6 +81,7 @@ async function renderAttributes(attributes, renderIsInheritable) {
|
|||||||
|
|
||||||
const HIDDEN_ATTRIBUTES = [
|
const HIDDEN_ATTRIBUTES = [
|
||||||
'originalFileName',
|
'originalFileName',
|
||||||
|
'fileSize',
|
||||||
'template',
|
'template',
|
||||||
'cssClass',
|
'cssClass',
|
||||||
'iconClass',
|
'iconClass',
|
||||||
|
@ -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,
|
||||||
|
@ -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];
|
||||||
|
@ -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"
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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!
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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() {
|
||||||
|
@ -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() {
|
||||||
|
@ -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());
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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()
|
||||||
});
|
});
|
||||||
|
@ -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%;
|
||||||
|
@ -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;
|
||||||
|
30
src/routes/api/etapi_tokens.js
Normal file
30
src/routes/api/etapi_tokens.js
Normal 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
|
||||||
|
};
|
@ -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 = {
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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.
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
|
||||||
};
|
|
@ -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,
|
||||||
|
@ -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(", "));
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
107
src/services/etapi_tokens.js
Normal file
107
src/services/etapi_tokens.js
Normal 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
|
||||||
|
};
|
5
src/services/member_id.js
Normal file
5
src/services/member_id.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
|
const instanceId = utils.randomString(12);
|
||||||
|
|
||||||
|
module.exports = instanceId;
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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
83
src/services/password.js
Normal 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
|
||||||
|
};
|
@ -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({
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 = "";
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user