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">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</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" />
|
||||
</component>
|
||||
</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,
|
||||
`isErased` INT NOT NULL,
|
||||
`changeId` TEXT NOT NULL,
|
||||
`sourceId` TEXT NOT NULL,
|
||||
`componentId` TEXT NOT NULL,
|
||||
`instanceId` TEXT NOT NULL,
|
||||
`isSynced` INTEGER 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,
|
||||
token TEXT NOT NULL,
|
||||
etapiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
tokenHash TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0);
|
||||
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_dateCreated` ON `note_revisions` (`dateCreated`);
|
||||
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
|
||||
on attributes (name, value);
|
||||
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) === "");
|
||||
}
|
||||
});
|
11481
package-lock.json
generated
11481
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -43,10 +43,10 @@
|
||||
"@electron/remote": "2.0.1",
|
||||
"express": "4.17.2",
|
||||
"express-partial-content": "^1.0.2",
|
||||
"express-rate-limit": "5.5.1",
|
||||
"express-rate-limit": "6.0.5",
|
||||
"express-session": "1.17.2",
|
||||
"fs-extra": "10.0.0",
|
||||
"helmet": "4.6.0",
|
||||
"helmet": "5.0.1",
|
||||
"html": "1.0.0",
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "5.0.0",
|
||||
@ -88,7 +88,7 @@
|
||||
"electron-packager": "15.4.0",
|
||||
"electron-rebuild": "3.2.5",
|
||||
"esm": "3.2.25",
|
||||
"jasmine": "3.10.0",
|
||||
"jasmine": "4.0.1",
|
||||
"jsdoc": "3.6.7",
|
||||
"lorem-ipsum": "2.0.4",
|
||||
"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", () => {
|
||||
it("simple lexing", () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
const Note = require('../../src/becca/entities/note.js');
|
||||
const Branch = require('../../src/becca/entities/branch.js');
|
||||
const Attribute = require('../../src/becca/entities/attribute.js');
|
||||
const becca = require('../../src/becca/becca.js');
|
||||
const Note = require('../../src/becca/entities/note');
|
||||
const Branch = require('../../src/becca/entities/branch');
|
||||
const Attribute = require('../../src/becca/entities/attribute');
|
||||
const becca = require('../../src/becca/becca');
|
||||
const randtoken = require('rand-token').generator({source: 'crypto'});
|
||||
|
||||
/** @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", () => {
|
||||
it("handles parens", () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
const SearchContext = require("../../src/services/search/search_context.js");
|
||||
const parse = require('../../src/services/search/services/parse.js');
|
||||
const SearchContext = require("../../src/services/search/search_context");
|
||||
const parse = require('../../src/services/search/services/parse');
|
||||
|
||||
function tokens(toks, cur = 0) {
|
||||
return toks.map(arg => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
const searchService = require('../../src/services/search/services/search.js');
|
||||
const Note = require('../../src/becca/entities/note.js');
|
||||
const Branch = require('../../src/becca/entities/branch.js');
|
||||
const SearchContext = require('../../src/services/search/search_context.js');
|
||||
const dateUtils = require('../../src/services/date_utils.js');
|
||||
const becca = require('../../src/becca/becca.js');
|
||||
const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking.js');
|
||||
const searchService = require('../../src/services/search/services/search');
|
||||
const Note = require('../../src/becca/entities/note');
|
||||
const Branch = require('../../src/becca/entities/branch');
|
||||
const SearchContext = require('../../src/services/search/search_context');
|
||||
const dateUtils = require('../../src/services/date_utils');
|
||||
const becca = require('../../src/becca/becca');
|
||||
const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking');
|
||||
|
||||
describe("Search", () => {
|
||||
let rootNote;
|
||||
|
@ -1,7 +1,7 @@
|
||||
const {note} = require('./note_cache_mocking.js');
|
||||
const ValueExtractor = require('../../src/services/search/value_extractor.js');
|
||||
const becca = require('../../src/becca/becca.js');
|
||||
const SearchContext = require("../../src/services/search/search_context.js");
|
||||
const {note} = require('./note_cache_mocking');
|
||||
const ValueExtractor = require('../../src/services/search/value_extractor');
|
||||
const becca = require('../../src/becca/becca');
|
||||
const SearchContext = require("../../src/services/search/search_context");
|
||||
|
||||
const dsc = new SearchContext();
|
||||
|
||||
|
@ -11,7 +11,7 @@ const sessionSecret = require('./services/session_secret');
|
||||
const dataDir = require('./services/data_dir');
|
||||
const utils = require('./services/utils');
|
||||
require('./services/handlers');
|
||||
require('./becca/becca_loader.js');
|
||||
require('./becca/becca_loader');
|
||||
|
||||
const app = express();
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require("../services/sql.js");
|
||||
const sql = require("../services/sql");
|
||||
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.
|
||||
@ -24,6 +25,8 @@ class Becca {
|
||||
this.attributeIndex = {};
|
||||
/** @type {Object.<String, Option>} */
|
||||
this.options = {};
|
||||
/** @type {Object.<String, EtapiToken>} */
|
||||
this.etapiTokens = {};
|
||||
|
||||
this.loaded = false;
|
||||
}
|
||||
@ -64,10 +67,12 @@ class Becca {
|
||||
this.dirtyNoteSetCache();
|
||||
}
|
||||
|
||||
/** @returns {Note|null} */
|
||||
getNote(noteId) {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
/** @returns {Note[]} */
|
||||
getNotes(noteIds, ignoreMissing = false) {
|
||||
const filteredNotes = [];
|
||||
|
||||
@ -88,29 +93,44 @@ class Becca {
|
||||
return filteredNotes;
|
||||
}
|
||||
|
||||
/** @returns {Branch|null} */
|
||||
getBranch(branchId) {
|
||||
return this.branches[branchId];
|
||||
}
|
||||
|
||||
/** @returns {Attribute|null} */
|
||||
getAttribute(attributeId) {
|
||||
return this.attributes[attributeId];
|
||||
}
|
||||
|
||||
/** @returns {Branch|null} */
|
||||
getBranchFromChildAndParent(childNoteId, parentNoteId) {
|
||||
return this.childParentToBranch[`${childNoteId}-${parentNoteId}`];
|
||||
}
|
||||
|
||||
/** @returns {NoteRevision|null} */
|
||||
getNoteRevision(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;
|
||||
}
|
||||
|
||||
/** @returns {Option|null} */
|
||||
getOption(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) {
|
||||
if (!entityName || !entityId) {
|
||||
return null;
|
||||
@ -130,17 +150,19 @@ class Becca {
|
||||
return this[camelCaseEntityName][entityId];
|
||||
}
|
||||
|
||||
/** @returns {RecentNote[]} */
|
||||
getRecentNotesFromQuery(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));
|
||||
}
|
||||
|
||||
/** @returns {NoteRevision[]} */
|
||||
getNoteRevisionsFromQuery(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));
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ const Note = require('./entities/note');
|
||||
const Branch = require('./entities/branch');
|
||||
const Attribute = require('./entities/attribute');
|
||||
const Option = require('./entities/option');
|
||||
const EtapiToken = require("./entities/etapi_token");
|
||||
const cls = require("../services/cls");
|
||||
const entityConstructor = require("../becca/entity_constructor");
|
||||
|
||||
@ -45,6 +46,10 @@ function load() {
|
||||
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) {
|
||||
becca.notes[noteId].sortParents();
|
||||
}
|
||||
@ -75,7 +80,7 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entity
|
||||
return;
|
||||
}
|
||||
|
||||
if (["notes", "branches", "attributes"].includes(entityName)) {
|
||||
if (["notes", "branches", "attributes", "etapi_tokens"].includes(entityName)) {
|
||||
const EntityClass = entityConstructor.getEntityFromEntityName(entityName);
|
||||
const primaryKeyName = EntityClass.primaryKeyName;
|
||||
|
||||
@ -112,6 +117,8 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENT
|
||||
branchDeleted(entityId);
|
||||
} else if (entityName === 'attributes') {
|
||||
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, () => {
|
||||
try {
|
||||
becca.decryptProtectedNotes();
|
||||
|
@ -40,7 +40,7 @@ class AbstractEntity {
|
||||
|
||||
get becca() {
|
||||
if (!becca) {
|
||||
becca = require('../becca.js');
|
||||
becca = require('../becca');
|
||||
}
|
||||
|
||||
return becca;
|
||||
@ -116,6 +116,19 @@ class AbstractEntity {
|
||||
|
||||
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;
|
||||
|
@ -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";
|
||||
|
||||
const Note = require('./note.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const sql = require("../../services/sql.js");
|
||||
const dateUtils = require("../../services/date_utils.js");
|
||||
const Note = require('./note');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
const sql = require("../../services/sql");
|
||||
const dateUtils = require("../../services/date_utils");
|
||||
const promotedAttributeDefinitionParser = require("../../services/promoted_attribute_definition_parser");
|
||||
|
||||
/**
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
const Note = require('./note.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const sql = require("../../services/sql.js");
|
||||
const dateUtils = require("../../services/date_utils.js");
|
||||
const Note = require('./note');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
const sql = require("../../services/sql");
|
||||
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
|
||||
|
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 dateUtils = require('../../services/date_utils');
|
||||
const entityChangesService = require('../../services/entity_changes');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const NoteRevision = require("./note_revision.js");
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
const NoteRevision = require("./note_revision");
|
||||
|
||||
const LABEL = 'label';
|
||||
const RELATION = 'relation';
|
||||
@ -984,7 +984,7 @@ class Note extends AbstractEntity {
|
||||
}
|
||||
}
|
||||
else {
|
||||
const Attribute = require("./attribute.js");
|
||||
const Attribute = require("./attribute");
|
||||
|
||||
new Attribute({
|
||||
noteId: this.noteId,
|
||||
@ -1016,7 +1016,7 @@ class Note extends AbstractEntity {
|
||||
* @return {Attribute}
|
||||
*/
|
||||
addAttribute(type, name, value = "", isInheritable = false, position = 1000) {
|
||||
const Attribute = require("./attribute.js");
|
||||
const Attribute = require("./attribute");
|
||||
|
||||
return new Attribute({
|
||||
noteId: this.noteId,
|
||||
|
@ -4,9 +4,9 @@ const protectedSessionService = require('../../services/protected_session');
|
||||
const utils = require('../../services/utils');
|
||||
const sql = require('../../services/sql');
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const becca = require('../becca.js');
|
||||
const becca = require('../becca');
|
||||
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.
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const dateUtils = require('../../services/date_utils.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
|
||||
/**
|
||||
* Option represents name-value pair, either directly configurable by the user or some system property.
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const dateUtils = require('../../services/date_utils.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
|
||||
/**
|
||||
* RecentNote represents recently visited note.
|
||||
|
@ -3,7 +3,7 @@ const NoteRevision = require('./entities/note_revision');
|
||||
const Branch = require('./entities/branch');
|
||||
const Attribute = require('./entities/attribute');
|
||||
const RecentNote = require('./entities/recent_note');
|
||||
const ApiToken = require('./entities/api_token');
|
||||
const EtapiToken = require('./entities/etapi_token');
|
||||
const Option = require('./entities/option');
|
||||
|
||||
const ENTITY_NAME_TO_ENTITY = {
|
||||
@ -14,7 +14,7 @@ const ENTITY_NAME_TO_ENTITY = {
|
||||
"note_revisions": NoteRevision,
|
||||
"note_revision_contents": NoteRevision,
|
||||
"recent_notes": RecentNote,
|
||||
"api_tokens": ApiToken,
|
||||
"etapi_tokens": EtapiToken,
|
||||
"options": Option
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
const becca = require('./becca.js');
|
||||
const becca = require('./becca');
|
||||
const log = require('../services/log');
|
||||
const beccaService = require('./becca_service.js');
|
||||
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");
|
||||
|
||||
export async function showDialog() {
|
||||
export async function showDialog(openTab) {
|
||||
const options = await server.get('options');
|
||||
|
||||
utils.openDialog($dialog);
|
||||
@ -14,7 +14,8 @@ export async function showDialog() {
|
||||
import('./options/appearance.js'),
|
||||
import('./options/shortcuts.js'),
|
||||
import('./options/code_notes.js'),
|
||||
import('./options/credentials.js'),
|
||||
import('./options/password.js'),
|
||||
import('./options/etapi.js'),
|
||||
import('./options/backup.js'),
|
||||
import('./options/sync.js'),
|
||||
import('./options/other.js'),
|
||||
@ -26,4 +27,8 @@ export async function showDialog() {
|
||||
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";
|
||||
|
||||
const TPL = `
|
||||
<h3>Username</h3>
|
||||
|
||||
<p>Your username is <strong id="credentials-username"></strong>.</p>
|
||||
|
||||
<h3>Change password</h3>
|
||||
<h3 id="password-heading"></h3>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<input class="form-control" id="old-password" type="password">
|
||||
</div>
|
||||
@ -29,24 +27,41 @@ const TPL = `
|
||||
<input class="form-control" id="new-password2" type="password">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary">Change password</button>
|
||||
<button class="btn btn-primary" id="save-password-button">Change password</button>
|
||||
</form>`;
|
||||
|
||||
export default class ChangePasswordOptions {
|
||||
constructor() {
|
||||
$("#options-credentials").html(TPL);
|
||||
$("#options-password").html(TPL);
|
||||
|
||||
this.$username = $("#credentials-username");
|
||||
this.$passwordHeading = $("#password-heading");
|
||||
this.$form = $("#change-password-form");
|
||||
this.$oldPassword = $("#old-password");
|
||||
this.$newPassword1 = $("#new-password1");
|
||||
this.$newPassword2 = $("#new-password2");
|
||||
this.$savePasswordButton = $("#save-password-button");
|
||||
this.$resetPasswordButton = $("#reset-password-button");
|
||||
|
||||
this.$resetPasswordButton.on("click", async () => {
|
||||
if (confirm("By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?")) {
|
||||
await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes");
|
||||
|
||||
const options = await server.get('options');
|
||||
this.optionsLoaded(options);
|
||||
|
||||
alert("Password has been reset. Please set new password");
|
||||
}
|
||||
});
|
||||
|
||||
this.$form.on('submit', () => this.save());
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
this.$username.text(options.username);
|
||||
const isPasswordSet = options.isPasswordSet === 'true';
|
||||
|
||||
$("#old-password-form-group").toggle(isPasswordSet);
|
||||
this.$passwordHeading.text(isPasswordSet ? 'Change password' : 'Set password');
|
||||
this.$savePasswordButton.text(isPasswordSet ? 'Change password' : 'Set password');
|
||||
}
|
||||
|
||||
save() {
|
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 shownCb;
|
||||
|
||||
export function ask({ message, defaultValue, shown }) {
|
||||
export function ask({ title, message, defaultValue, shown }) {
|
||||
shownCb = shown;
|
||||
|
||||
|
||||
$("#prompt-title").text(title || "Prompt");
|
||||
|
||||
$question = $("<label>")
|
||||
.prop("for", "prompt-dialog-answer")
|
||||
.text(message);
|
||||
@ -30,7 +32,7 @@ export function ask({ message, defaultValue, shown }) {
|
||||
.append($question)
|
||||
.append($answer));
|
||||
|
||||
utils.openDialog($dialog);
|
||||
utils.openDialog($dialog, false);
|
||||
|
||||
return new Promise((res, rej) => { resolve = res; });
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export function show() {
|
||||
}
|
||||
|
||||
export function close() {
|
||||
// this may fal if the dialog has not been previously opened (not sure if still true with Bootstrap modal)
|
||||
// this may fail if the dialog has not been previously opened (not sure if still true with Bootstrap modal)
|
||||
try {
|
||||
$dialog.modal('hide');
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ class AppContext extends Component {
|
||||
const appContext = new AppContext(window.glob.isMainWindow);
|
||||
|
||||
// we should save all outstanding changes before the page/app is closed
|
||||
$(window).on('beforeunload', () => {
|
||||
$(window).on('beforeunload', () => {return "SSS";
|
||||
let allSaved = true;
|
||||
|
||||
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref());
|
||||
|
@ -81,6 +81,7 @@ async function renderAttributes(attributes, renderIsInheritable) {
|
||||
|
||||
const HIDDEN_ATTRIBUTES = [
|
||||
'originalFileName',
|
||||
'fileSize',
|
||||
'template',
|
||||
'cssClass',
|
||||
'iconClass',
|
||||
|
@ -11,12 +11,12 @@ async function getInboxNote() {
|
||||
|
||||
/** @returns {NoteShort} */
|
||||
async function getTodayNote() {
|
||||
return await getDateNote(dayjs().format("YYYY-MM-DD"));
|
||||
return await getDayNote(dayjs().format("YYYY-MM-DD"));
|
||||
}
|
||||
|
||||
/** @returns {NoteShort} */
|
||||
async function getDateNote(date) {
|
||||
const note = await server.get('special-notes/date/' + date, "date-note");
|
||||
async function getDayNote(date) {
|
||||
const note = await server.get('special-notes/days/' + date, "date-note");
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
@ -25,7 +25,7 @@ async function getDateNote(date) {
|
||||
|
||||
/** @returns {NoteShort} */
|
||||
async function getWeekNote(date) {
|
||||
const note = await server.get('special-notes/week/' + date, "date-note");
|
||||
const note = await server.get('special-notes/weeks/' + date, "date-note");
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
@ -34,7 +34,7 @@ async function getWeekNote(date) {
|
||||
|
||||
/** @returns {NoteShort} */
|
||||
async function getMonthNote(month) {
|
||||
const note = await server.get('special-notes/month/' + month, "date-note");
|
||||
const note = await server.get('special-notes/months/' + month, "date-note");
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
@ -43,7 +43,7 @@ async function getMonthNote(month) {
|
||||
|
||||
/** @returns {NoteShort} */
|
||||
async function getYearNote(year) {
|
||||
const note = await server.get('special-notes/year/' + year, "date-note");
|
||||
const note = await server.get('special-notes/years/' + year, "date-note");
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
@ -71,7 +71,7 @@ async function createSearchNote(opts = {}) {
|
||||
export default {
|
||||
getInboxNote,
|
||||
getTodayNote,
|
||||
getDateNote,
|
||||
getDayNote,
|
||||
getWeekNote,
|
||||
getMonthNote,
|
||||
getYearNote,
|
||||
|
@ -22,9 +22,9 @@ async function processEntityChanges(entityChanges) {
|
||||
} else if (ec.entityName === 'note_contents') {
|
||||
delete froca.noteComplementPromises[ec.entityId];
|
||||
|
||||
loadResults.addNoteContent(ec.entityId, ec.sourceId);
|
||||
loadResults.addNoteContent(ec.entityId, ec.componentId);
|
||||
} else if (ec.entityName === 'note_revisions') {
|
||||
loadResults.addNoteRevision(ec.entityId, ec.noteId, ec.sourceId);
|
||||
loadResults.addNoteRevision(ec.entityId, ec.noteId, ec.componentId);
|
||||
} else if (ec.entityName === 'note_revision_contents') {
|
||||
// this should change only when toggling isProtected, ignore
|
||||
} else if (ec.entityName === 'options') {
|
||||
@ -36,6 +36,9 @@ async function processEntityChanges(entityChanges) {
|
||||
|
||||
loadResults.addOption(ec.entity.name);
|
||||
}
|
||||
else if (ec.entityName === 'etapi_tokens') {
|
||||
// NOOP
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unknown entityName ${ec.entityName}`);
|
||||
}
|
||||
@ -87,7 +90,7 @@ function processNoteChange(loadResults, ec) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadResults.addNote(ec.entityId, ec.sourceId);
|
||||
loadResults.addNote(ec.entityId, ec.componentId);
|
||||
|
||||
if (ec.isErased && ec.entityId in froca.notes) {
|
||||
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
|
||||
@ -125,7 +128,7 @@ function processBranchChange(loadResults, ec) {
|
||||
delete parentNote.childToBranch[branch.noteId];
|
||||
}
|
||||
|
||||
loadResults.addBranch(ec.entityId, ec.sourceId);
|
||||
loadResults.addBranch(ec.entityId, ec.componentId);
|
||||
|
||||
delete froca.branches[ec.entityId];
|
||||
}
|
||||
@ -133,7 +136,7 @@ function processBranchChange(loadResults, ec) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadResults.addBranch(ec.entityId, ec.sourceId);
|
||||
loadResults.addBranch(ec.entityId, ec.componentId);
|
||||
|
||||
const childNote = froca.notes[ec.entity.noteId];
|
||||
const parentNote = froca.notes[ec.entity.parentNoteId];
|
||||
@ -175,7 +178,7 @@ function processNoteReordering(loadResults, ec) {
|
||||
}
|
||||
}
|
||||
|
||||
loadResults.addNoteReordering(ec.entityId, ec.sourceId);
|
||||
loadResults.addNoteReordering(ec.entityId, ec.componentId);
|
||||
}
|
||||
|
||||
function processAttributeChange(loadResults, ec) {
|
||||
@ -199,7 +202,7 @@ function processAttributeChange(loadResults, ec) {
|
||||
targetNote.targetRelations = targetNote.targetRelations.filter(attributeId => attributeId !== attribute.attributeId);
|
||||
}
|
||||
|
||||
loadResults.addAttribute(ec.entityId, ec.sourceId);
|
||||
loadResults.addAttribute(ec.entityId, ec.componentId);
|
||||
|
||||
delete froca.attributes[ec.entityId];
|
||||
}
|
||||
@ -207,7 +210,7 @@ function processAttributeChange(loadResults, ec) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadResults.addAttribute(ec.entityId, ec.sourceId);
|
||||
loadResults.addAttribute(ec.entityId, ec.componentId);
|
||||
|
||||
const sourceNote = froca.notes[ec.entity.noteId];
|
||||
const targetNote = ec.entity.type === 'relation' && froca.notes[ec.entity.value];
|
||||
|
@ -389,16 +389,26 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
this.getTodayNote = dateNotesService.getTodayNote;
|
||||
|
||||
/**
|
||||
* Returns date-note. If it doesn't exist, it is automatically created.
|
||||
* Returns day note for a given date. If it doesn't exist, it is automatically created.
|
||||
*
|
||||
* @method
|
||||
* @param {string} date - e.g. "2019-04-29"
|
||||
* @return {Promise<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
|
||||
* @param {string} date - e.g. "2019-04-29"
|
||||
* @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
|
||||
* @param {string} date - e.g. "2019-04-29"
|
||||
|
@ -3,16 +3,17 @@ const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
|
||||
const CODE_MIRROR = {
|
||||
js: [
|
||||
"libraries/codemirror/codemirror.js",
|
||||
"libraries/codemirror/addon/mode/loadmode.js",
|
||||
"libraries/codemirror/addon/mode/simple.js",
|
||||
"libraries/codemirror/addon/fold/xml-fold.js",
|
||||
"libraries/codemirror/addon/display/placeholder.js",
|
||||
"libraries/codemirror/addon/edit/matchbrackets.js",
|
||||
"libraries/codemirror/addon/edit/matchtags.js",
|
||||
"libraries/codemirror/addon/fold/xml-fold.js",
|
||||
"libraries/codemirror/addon/lint/lint.js",
|
||||
"libraries/codemirror/addon/lint/eslint.js",
|
||||
"libraries/codemirror/addon/mode/loadmode.js",
|
||||
"libraries/codemirror/addon/mode/simple.js",
|
||||
"libraries/codemirror/addon/search/match-highlighter.js",
|
||||
"libraries/codemirror/mode/meta.js",
|
||||
"libraries/codemirror/keymap/vim.js",
|
||||
"libraries/codemirror/addon/lint/lint.js",
|
||||
"libraries/codemirror/addon/lint/eslint.js"
|
||||
"libraries/codemirror/keymap/vim.js"
|
||||
],
|
||||
css: [
|
||||
"libraries/codemirror/codemirror.css",
|
||||
|
@ -9,8 +9,8 @@ export default class LoadResults {
|
||||
}
|
||||
}
|
||||
|
||||
this.noteIdToSourceId = {};
|
||||
this.sourceIdToNoteIds = {};
|
||||
this.noteIdToComponentId = {};
|
||||
this.componentIdToNoteIds = {};
|
||||
|
||||
this.branches = [];
|
||||
|
||||
@ -20,7 +20,7 @@ export default class LoadResults {
|
||||
|
||||
this.noteRevisions = [];
|
||||
|
||||
this.contentNoteIdToSourceId = [];
|
||||
this.contentNoteIdToComponentId = [];
|
||||
|
||||
this.options = [];
|
||||
}
|
||||
@ -29,22 +29,22 @@ export default class LoadResults {
|
||||
return this.entities[entityName]?.[entityId];
|
||||
}
|
||||
|
||||
addNote(noteId, sourceId) {
|
||||
this.noteIdToSourceId[noteId] = this.noteIdToSourceId[noteId] || [];
|
||||
addNote(noteId, componentId) {
|
||||
this.noteIdToComponentId[noteId] = this.noteIdToComponentId[noteId] || [];
|
||||
|
||||
if (!this.noteIdToSourceId[noteId].includes(sourceId)) {
|
||||
this.noteIdToSourceId[noteId].push(sourceId);
|
||||
if (!this.noteIdToComponentId[noteId].includes(componentId)) {
|
||||
this.noteIdToComponentId[noteId].push(componentId);
|
||||
}
|
||||
|
||||
this.sourceIdToNoteIds[sourceId] = this.sourceIdToNoteIds[sourceId] || [];
|
||||
this.componentIdToNoteIds[componentId] = this.componentIdToNoteIds[componentId] || [];
|
||||
|
||||
if (!this.sourceIdToNoteIds[sourceId]) {
|
||||
this.sourceIdToNoteIds[sourceId].push(noteId);
|
||||
if (!this.componentIdToNoteIds[componentId]) {
|
||||
this.componentIdToNoteIds[componentId].push(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
addBranch(branchId, sourceId) {
|
||||
this.branches.push({branchId, sourceId});
|
||||
addBranch(branchId, componentId) {
|
||||
this.branches.push({branchId, componentId});
|
||||
}
|
||||
|
||||
getBranches() {
|
||||
@ -53,7 +53,7 @@ export default class LoadResults {
|
||||
.filter(branch => !!branch);
|
||||
}
|
||||
|
||||
addNoteReordering(parentNoteId, sourceId) {
|
||||
addNoteReordering(parentNoteId, componentId) {
|
||||
this.noteReorderings.push(parentNoteId);
|
||||
}
|
||||
|
||||
@ -61,20 +61,20 @@ export default class LoadResults {
|
||||
return this.noteReorderings;
|
||||
}
|
||||
|
||||
addAttribute(attributeId, sourceId) {
|
||||
this.attributes.push({attributeId, sourceId});
|
||||
addAttribute(attributeId, componentId) {
|
||||
this.attributes.push({attributeId, componentId});
|
||||
}
|
||||
|
||||
/** @returns {Attribute[]} */
|
||||
getAttributes(sourceId = 'none') {
|
||||
getAttributes(componentId = 'none') {
|
||||
return this.attributes
|
||||
.filter(row => row.sourceId !== sourceId)
|
||||
.filter(row => row.componentId !== componentId)
|
||||
.map(row => this.getEntity("attributes", row.attributeId))
|
||||
.filter(attr => !!attr);
|
||||
}
|
||||
|
||||
addNoteRevision(noteRevisionId, noteId, sourceId) {
|
||||
this.noteRevisions.push({noteRevisionId, noteId, sourceId});
|
||||
addNoteRevision(noteRevisionId, noteId, componentId) {
|
||||
this.noteRevisions.push({noteRevisionId, noteId, componentId});
|
||||
}
|
||||
|
||||
hasNoteRevisionForNote(noteId) {
|
||||
@ -82,28 +82,28 @@ export default class LoadResults {
|
||||
}
|
||||
|
||||
getNoteIds() {
|
||||
return Object.keys(this.noteIdToSourceId);
|
||||
return Object.keys(this.noteIdToComponentId);
|
||||
}
|
||||
|
||||
isNoteReloaded(noteId, sourceId = null) {
|
||||
isNoteReloaded(noteId, componentId = null) {
|
||||
if (!noteId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sourceIds = this.noteIdToSourceId[noteId];
|
||||
return sourceIds && !!sourceIds.find(sId => sId !== sourceId);
|
||||
const componentIds = this.noteIdToComponentId[noteId];
|
||||
return componentIds && !!componentIds.find(sId => sId !== componentId);
|
||||
}
|
||||
|
||||
addNoteContent(noteId, sourceId) {
|
||||
this.contentNoteIdToSourceId.push({noteId, sourceId});
|
||||
addNoteContent(noteId, componentId) {
|
||||
this.contentNoteIdToComponentId.push({noteId, componentId});
|
||||
}
|
||||
|
||||
isNoteContentReloaded(noteId, sourceId) {
|
||||
isNoteContentReloaded(noteId, componentId) {
|
||||
if (!noteId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.contentNoteIdToSourceId.find(l => l.noteId === noteId && l.sourceId !== sourceId);
|
||||
return this.contentNoteIdToComponentId.find(l => l.noteId === noteId && l.componentId !== componentId);
|
||||
}
|
||||
|
||||
addOption(name) {
|
||||
@ -124,17 +124,17 @@ export default class LoadResults {
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return Object.keys(this.noteIdToSourceId).length === 0
|
||||
return Object.keys(this.noteIdToComponentId).length === 0
|
||||
&& this.branches.length === 0
|
||||
&& this.attributes.length === 0
|
||||
&& this.noteReorderings.length === 0
|
||||
&& this.noteRevisions.length === 0
|
||||
&& this.contentNoteIdToSourceId.length === 0
|
||||
&& this.contentNoteIdToComponentId.length === 0
|
||||
&& this.options.length === 0;
|
||||
}
|
||||
|
||||
isEmptyForTree() {
|
||||
return Object.keys(this.noteIdToSourceId).length === 0
|
||||
return Object.keys(this.noteIdToComponentId).length === 0
|
||||
&& this.branches.length === 0
|
||||
&& this.attributes.length === 0
|
||||
&& this.noteReorderings.length === 0;
|
||||
|
@ -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;
|
||||
|
@ -5,6 +5,7 @@ import ws from "./ws.js";
|
||||
import appContext from "./app_context.js";
|
||||
import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
import options from "./options.js";
|
||||
|
||||
let protectedSessionDeferred = null;
|
||||
|
||||
@ -18,6 +19,11 @@ async function leaveProtectedSession() {
|
||||
function enterProtectedSession() {
|
||||
const dfd = $.Deferred();
|
||||
|
||||
if (!options.is("isPasswordSet")) {
|
||||
import("../dialogs/password_not_set.js").then(dialog => dialog.show());
|
||||
return dfd;
|
||||
}
|
||||
|
||||
if (protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||
dfd.resolve(false);
|
||||
}
|
||||
|
@ -53,8 +53,8 @@ export default class RootCommandExecutor extends Component {
|
||||
d.showDialog(branchIds);
|
||||
}
|
||||
|
||||
showOptionsCommand() {
|
||||
import("../dialogs/options.js").then(d => d.showDialog());
|
||||
showOptionsCommand({openTab}) {
|
||||
import("../dialogs/options.js").then(d => d.showDialog(openTab));
|
||||
}
|
||||
|
||||
showHelpCommand() {
|
||||
|
@ -9,7 +9,7 @@ async function getHeaders(headers) {
|
||||
// headers need to be lowercase because node.js automatically converts them to lower case
|
||||
// also avoiding using underscores instead of dashes since nginx filters them out by default
|
||||
const allHeaders = {
|
||||
'trilium-source-id': glob.sourceId,
|
||||
'trilium-component-id': glob.componentId,
|
||||
'trilium-local-now-datetime': utils.localNowDateTime(),
|
||||
'trilium-hoisted-note-id': activeNoteContext ? activeNoteContext.hoistedNoteId : null,
|
||||
'x-csrf-token': glob.csrfToken
|
||||
@ -29,20 +29,24 @@ async function getHeaders(headers) {
|
||||
return allHeaders;
|
||||
}
|
||||
|
||||
async function get(url, sourceId) {
|
||||
return await call('GET', url, null, {'trilium-source-id': sourceId});
|
||||
async function get(url, componentId) {
|
||||
return await call('GET', url, null, {'trilium-component-id': componentId});
|
||||
}
|
||||
|
||||
async function post(url, data, sourceId) {
|
||||
return await call('POST', url, data, {'trilium-source-id': sourceId});
|
||||
async function post(url, data, componentId) {
|
||||
return await call('POST', url, data, {'trilium-component-id': componentId});
|
||||
}
|
||||
|
||||
async function put(url, data, sourceId) {
|
||||
return await call('PUT', url, data, {'trilium-source-id': sourceId});
|
||||
async function put(url, data, componentId) {
|
||||
return await call('PUT', url, data, {'trilium-component-id': componentId});
|
||||
}
|
||||
|
||||
async function remove(url, sourceId) {
|
||||
return await call('DELETE', url, null, {'trilium-source-id': sourceId});
|
||||
async function patch(url, data, componentId) {
|
||||
return await call('PATCH', url, data, {'trilium-component-id': componentId});
|
||||
}
|
||||
|
||||
async function remove(url, componentId) {
|
||||
return await call('DELETE', url, null, {'trilium-component-id': componentId});
|
||||
}
|
||||
|
||||
let i = 1;
|
||||
@ -185,6 +189,7 @@ export default {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
patch,
|
||||
remove,
|
||||
ajax,
|
||||
// don't remove, used from CKEditor image upload!
|
||||
|
@ -245,10 +245,11 @@ function focusSavedElement() {
|
||||
$lastFocusedElement = null;
|
||||
}
|
||||
|
||||
async function openDialog($dialog) {
|
||||
closeActiveDialog();
|
||||
|
||||
glob.activeDialog = $dialog;
|
||||
async function openDialog($dialog, closeActDialog = true) {
|
||||
if (closeActDialog) {
|
||||
closeActiveDialog();
|
||||
glob.activeDialog = $dialog;
|
||||
}
|
||||
|
||||
saveFocusedElement();
|
||||
|
||||
|
@ -19,20 +19,23 @@ function SetupModel() {
|
||||
this.setupSyncFromDesktop = ko.observable(false);
|
||||
this.setupSyncFromServer = ko.observable(false);
|
||||
|
||||
this.username = ko.observable();
|
||||
this.password1 = ko.observable();
|
||||
this.password2 = ko.observable();
|
||||
|
||||
this.theme = ko.observable("light");
|
||||
this.syncServerHost = ko.observable();
|
||||
this.syncProxy = ko.observable();
|
||||
|
||||
this.instanceType = utils.isElectron() ? "desktop" : "server";
|
||||
this.password = ko.observable();
|
||||
|
||||
this.setupTypeSelected = () => !!this.setupType();
|
||||
|
||||
this.selectSetupType = () => {
|
||||
this.step(this.setupType());
|
||||
if (this.setupType() === 'new-document') {
|
||||
this.step('new-document-in-progress');
|
||||
|
||||
$.post('api/setup/new-document').then(() => {
|
||||
window.location.replace("./setup");
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.step(this.setupType());
|
||||
}
|
||||
};
|
||||
|
||||
this.back = () => {
|
||||
@ -42,77 +45,36 @@ function SetupModel() {
|
||||
};
|
||||
|
||||
this.finish = async () => {
|
||||
if (this.setupType() === 'new-document') {
|
||||
const username = this.username();
|
||||
const password1 = this.password1();
|
||||
const password2 = this.password2();
|
||||
const theme = this.theme();
|
||||
const syncServerHost = this.syncServerHost();
|
||||
const syncProxy = this.syncProxy();
|
||||
const password = this.password();
|
||||
|
||||
if (!username) {
|
||||
showAlert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password1) {
|
||||
showAlert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password1 !== password2) {
|
||||
showAlert("Both password fields need be identical.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.step('new-document-in-progress');
|
||||
|
||||
// not using server.js because it loads too many dependencies
|
||||
$.post('api/setup/new-document', {
|
||||
username: username,
|
||||
password: password1,
|
||||
theme: theme
|
||||
}).then(() => {
|
||||
window.location.replace("./setup");
|
||||
});
|
||||
if (!syncServerHost) {
|
||||
showAlert("Trilium server address can't be empty");
|
||||
return;
|
||||
}
|
||||
else if (this.setupType() === 'sync-from-server') {
|
||||
const syncServerHost = this.syncServerHost();
|
||||
const syncProxy = this.syncProxy();
|
||||
const username = this.username();
|
||||
const password = this.password1();
|
||||
|
||||
if (!syncServerHost) {
|
||||
showAlert("Trilium server address can't be empty");
|
||||
return;
|
||||
}
|
||||
if (!password) {
|
||||
showAlert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
showAlert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
// not using server.js because it loads too many dependencies
|
||||
const resp = await $.post('api/setup/sync-from-server', {
|
||||
syncServerHost: syncServerHost,
|
||||
syncProxy: syncProxy,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (!password) {
|
||||
showAlert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
if (resp.result === 'success') {
|
||||
this.step('sync-in-progress');
|
||||
|
||||
// not using server.js because it loads too many dependencies
|
||||
const resp = await $.post('api/setup/sync-from-server', {
|
||||
syncServerHost: syncServerHost,
|
||||
syncProxy: syncProxy,
|
||||
username: username,
|
||||
password: password
|
||||
});
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
|
||||
if (resp.result === 'success') {
|
||||
this.step('sync-in-progress');
|
||||
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
|
||||
hideAlert();
|
||||
}
|
||||
else {
|
||||
showAlert('Sync setup failed: ' + resp.error);
|
||||
}
|
||||
hideAlert();
|
||||
}
|
||||
else {
|
||||
showAlert('Sync setup failed: ' + resp.error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
|
||||
this.$dropdownContent.on('click', '.calendar-date', async ev => {
|
||||
const date = $(ev.target).closest('.calendar-date').attr('data-calendar-date');
|
||||
|
||||
const note = await dateNoteService.getDateNote(date);
|
||||
const note = await dateNoteService.getDayNote(date);
|
||||
|
||||
if (note) {
|
||||
appContext.tabManager.getActiveContext().setNote(note.noteId);
|
||||
|
@ -38,7 +38,7 @@ export default class RootContainer extends FlexContainer {
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
const note = appContext.tabManager.getActiveContextNote();
|
||||
|
||||
|
||||
if (note && loadResults.isNoteReloaded(note.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
|
@ -29,6 +29,10 @@ const TPL = `
|
||||
font-family: var(--detail-font-family);
|
||||
font-size: var(--detail-font-size);
|
||||
}
|
||||
|
||||
.note-detail.full-height {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`;
|
||||
@ -128,7 +132,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
await typeWidget.handleEvent('setNoteContext', {noteContext: this.noteContext});
|
||||
|
||||
// this is happening in update() so note has been already set and we need to reflect this
|
||||
// this is happening in update() so note has been already set, and we need to reflect this
|
||||
await typeWidget.handleEvent('noteSwitched', {
|
||||
noteContext: this.noteContext,
|
||||
notePath: this.noteContext.notePath
|
||||
@ -136,6 +140,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.child(typeWidget);
|
||||
}
|
||||
|
||||
this.checkFullHeight();
|
||||
}
|
||||
|
||||
checkFullHeight() {
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
this.$widget.toggleClass("full-height",
|
||||
!this.noteContext.hasNoteList()
|
||||
&& ['editable-text', 'editable-code'].includes(this.type));
|
||||
}
|
||||
|
||||
getTypeWidget() {
|
||||
|
@ -5,8 +5,6 @@ const TPL = `
|
||||
<div class="note-list-widget">
|
||||
<style>
|
||||
.note-list-widget {
|
||||
flex-grow: 100000;
|
||||
flex-shrink: 100000;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
@ -22,11 +20,7 @@ const TPL = `
|
||||
|
||||
export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& ['book', 'text', 'code'].includes(this.note.type)
|
||||
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
|
||||
&& this.note.hasChildren()
|
||||
&& !this.note.hasLabel('hideChildrenOverview');
|
||||
return super.isEnabled() && this.noteContext.hasNoteList();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
|
@ -13,10 +13,12 @@ const TPL = `
|
||||
<style>
|
||||
.note-detail-code {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-code-editor {
|
||||
min-height: 50px;
|
||||
height: 100%;
|
||||
}
|
||||
</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
|
||||
// all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem
|
||||
lineWrapping: true,
|
||||
dragDrop: false // with true the editor inlines dropped files which is not what we expect
|
||||
dragDrop: false, // with true the editor inlines dropped files which is not what we expect
|
||||
placeholder: "Type the content of your code note here..."
|
||||
});
|
||||
|
||||
this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate());
|
||||
|
@ -36,6 +36,7 @@ const TPL = `
|
||||
font-family: var(--detail-font-family);
|
||||
padding-left: 14px;
|
||||
padding-top: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-editable-text a:hover {
|
||||
@ -73,6 +74,7 @@ const TPL = `
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
min-height: 50px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -219,6 +219,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
else if (command === "editTitle") {
|
||||
const promptDialog = await import("../../dialogs/prompt.js");
|
||||
const title = await promptDialog.ask({
|
||||
title: "Rename note",
|
||||
message: "Enter new note title:",
|
||||
defaultValue: $title.text()
|
||||
});
|
||||
|
@ -241,6 +241,10 @@ body .CodeMirror {
|
||||
background-color: #eeeeee
|
||||
}
|
||||
|
||||
.CodeMirror pre.CodeMirror-placeholder {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
#sql-console-query {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
|
@ -36,7 +36,7 @@ function getClipperInboxNote() {
|
||||
let clipperInbox = attributeService.getNoteWithLabel('clipperInbox');
|
||||
|
||||
if (!clipperInbox) {
|
||||
clipperInbox = dateNoteService.getDateNote(dateUtils.localNowDate());
|
||||
clipperInbox = dateNoteService.getDayNote(dateUtils.localNowDate());
|
||||
}
|
||||
|
||||
return clipperInbox;
|
||||
|
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 utils = require('../../services/utils');
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const sourceIdService = require('../../services/source_id');
|
||||
const instanceId = require('../../services/member_id');
|
||||
const passwordEncryptionService = require('../../services/password_encryption');
|
||||
const protectedSessionService = require('../../services/protected_session');
|
||||
const appInfo = require('../../services/app_info');
|
||||
const eventService = require('../../services/events');
|
||||
const sqlInit = require('../../services/sql_init');
|
||||
const sql = require('../../services/sql');
|
||||
const optionService = require('../../services/options');
|
||||
const ApiToken = require('../../becca/entities/api_token');
|
||||
const ws = require("../../services/ws");
|
||||
const etapiTokenService = require("../../services/etapi_tokens");
|
||||
|
||||
function loginSync(req) {
|
||||
if (!sqlInit.schemaExists()) {
|
||||
@ -48,7 +47,7 @@ function loginSync(req) {
|
||||
req.session.loggedIn = true;
|
||||
|
||||
return {
|
||||
sourceId: sourceIdService.getCurrentSourceId(),
|
||||
instanceId: instanceId,
|
||||
maxEntityChangeId: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1")
|
||||
};
|
||||
}
|
||||
@ -85,23 +84,18 @@ function logoutFromProtectedSession() {
|
||||
}
|
||||
|
||||
function token(req) {
|
||||
const username = req.body.username;
|
||||
const password = req.body.password;
|
||||
|
||||
const isUsernameValid = username === optionService.getOption('username');
|
||||
const isPasswordValid = passwordEncryptionService.verifyPassword(password);
|
||||
|
||||
if (!isUsernameValid || !isPasswordValid) {
|
||||
return [401, "Incorrect username/password"];
|
||||
if (!passwordEncryptionService.verifyPassword(password)) {
|
||||
return [401, "Incorrect password"];
|
||||
}
|
||||
|
||||
const apiToken = new ApiToken({
|
||||
token: utils.randomSecureToken()
|
||||
}).save();
|
||||
// for backwards compatibility with Sender which does not send the name
|
||||
const tokenName = req.body.tokenName || "Trilium Sender / Web Clipper";
|
||||
|
||||
const {authToken} = etapiTokenService.createToken(tokenName);
|
||||
|
||||
return {
|
||||
token: apiToken.token
|
||||
};
|
||||
return { token: authToken };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -73,9 +73,7 @@ function deleteNote(req) {
|
||||
|
||||
const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
|
||||
|
||||
for (const branch of note.getParentBranches()) {
|
||||
noteService.deleteBranch(branch, deleteId, taskContext);
|
||||
}
|
||||
noteService.deleteNote(note, deleteId, taskContext);
|
||||
|
||||
if (eraseNotes) {
|
||||
noteService.eraseNotesWithDeleteId(deleteId);
|
||||
|
@ -6,7 +6,6 @@ const searchService = require('../../services/search/services/search');
|
||||
|
||||
// options allowed to be updated directly in options dialog
|
||||
const ALLOWED_OPTIONS = new Set([
|
||||
'username', // not exposed for update (not harmful anyway), needed for reading
|
||||
'eraseEntitiesAfterTimeInSeconds',
|
||||
'protectedSessionTimeout',
|
||||
'noteRevisionSnapshotTimeInterval',
|
||||
@ -69,6 +68,8 @@ function getOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
resultMap['isPasswordSet'] = !!optionMap['passwordVerificationHash'] ? 'true' : 'false';
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,26 @@
|
||||
"use strict";
|
||||
|
||||
const changePasswordService = require('../../services/change_password');
|
||||
const passwordService = require('../../services/password');
|
||||
|
||||
function changePassword(req) {
|
||||
return changePasswordService.changePassword(req.body.current_password, req.body.new_password);
|
||||
if (passwordService.isPasswordSet()) {
|
||||
return passwordService.changePassword(req.body.current_password, req.body.new_password);
|
||||
}
|
||||
else {
|
||||
return passwordService.setPassword(req.body.new_password);
|
||||
}
|
||||
}
|
||||
|
||||
function resetPassword(req) {
|
||||
// protection against accidental call (not a security measure)
|
||||
if (req.query.really !== "yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes") {
|
||||
return [400, "Incorrect password reset confirmation"];
|
||||
}
|
||||
|
||||
return passwordService.resetPassword();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
changePassword
|
||||
changePassword,
|
||||
resetPassword
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ function uploadImage(req) {
|
||||
|
||||
const originalName = "Sender image." + imageType(file.buffer).ext;
|
||||
|
||||
const parentNote = dateNoteService.getDateNote(req.headers['x-local-date']);
|
||||
const parentNote = dateNoteService.getDayNote(req.headers['x-local-date']);
|
||||
|
||||
const {note, noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
|
||||
|
||||
@ -35,7 +35,7 @@ function uploadImage(req) {
|
||||
}
|
||||
|
||||
function saveNote(req) {
|
||||
const parentNote = dateNoteService.getDateNote(req.headers['x-local-date']);
|
||||
const parentNote = dateNoteService.getDayNote(req.headers['x-local-date']);
|
||||
|
||||
const {note, branch} = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
|
@ -13,16 +13,14 @@ function getStatus() {
|
||||
};
|
||||
}
|
||||
|
||||
async function setupNewDocument(req) {
|
||||
const { username, password, theme } = req.body;
|
||||
|
||||
await sqlInit.createInitialDatabase(username, password, theme);
|
||||
async function setupNewDocument() {
|
||||
await sqlInit.createInitialDatabase();
|
||||
}
|
||||
|
||||
function setupSyncFromServer(req) {
|
||||
const { syncServerHost, syncProxy, username, password } = req.body;
|
||||
const { syncServerHost, syncProxy, password } = req.body;
|
||||
|
||||
return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, username, password);
|
||||
return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, password);
|
||||
}
|
||||
|
||||
function saveSyncSeed(req) {
|
||||
|
@ -10,8 +10,8 @@ function getInboxNote(req) {
|
||||
return specialNotesService.getInboxNote(req.params.date);
|
||||
}
|
||||
|
||||
function getDateNote(req) {
|
||||
return dateNoteService.getDateNote(req.params.date);
|
||||
function getDayNote(req) {
|
||||
return dateNoteService.getDayNote(req.params.date);
|
||||
}
|
||||
|
||||
function getWeekNote(req) {
|
||||
@ -26,7 +26,7 @@ function getYearNote(req) {
|
||||
return dateNoteService.getYearNote(req.params.year);
|
||||
}
|
||||
|
||||
function getDateNotesForMonth(req) {
|
||||
function getDayNotesForMonth(req) {
|
||||
const month = req.params.month;
|
||||
|
||||
return sql.getMap(`
|
||||
@ -68,11 +68,11 @@ function getHoistedNote() {
|
||||
|
||||
module.exports = {
|
||||
getInboxNote,
|
||||
getDateNote,
|
||||
getDayNote,
|
||||
getWeekNote,
|
||||
getMonthNote,
|
||||
getYearNote,
|
||||
getDateNotesForMonth,
|
||||
getDayNotesForMonth,
|
||||
createSqlConsole,
|
||||
saveSqlConsole,
|
||||
createSearchNote,
|
||||
|
@ -123,13 +123,45 @@ function forceNoteSync(req) {
|
||||
function getChanged(req) {
|
||||
const startTime = Date.now();
|
||||
|
||||
const lastEntityChangeId = parseInt(req.query.lastEntityChangeId);
|
||||
let lastEntityChangeId = parseInt(req.query.lastEntityChangeId);
|
||||
const clientinstanceId = req.query.instanceId;
|
||||
let filteredEntityChanges = [];
|
||||
|
||||
const entityChanges = sql.getRows("SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000", [lastEntityChangeId]);
|
||||
while (filteredEntityChanges.length === 0) {
|
||||
const entityChanges = sql.getRows(`
|
||||
SELECT *
|
||||
FROM entity_changes
|
||||
WHERE isSynced = 1
|
||||
AND id > ?
|
||||
ORDER BY id
|
||||
LIMIT 1000`, [lastEntityChangeId]);
|
||||
|
||||
if (entityChanges.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
filteredEntityChanges = entityChanges.filter(ec => ec.instanceId !== clientinstanceId);
|
||||
|
||||
if (filteredEntityChanges.length === 0) {
|
||||
lastEntityChangeId = entityChanges[entityChanges.length - 1].id;
|
||||
}
|
||||
}
|
||||
|
||||
const entityChangeRecords = syncService.getEntityChangeRecords(filteredEntityChanges);
|
||||
|
||||
if (entityChangeRecords.length > 0) {
|
||||
lastEntityChangeId = entityChangeRecords[entityChangeRecords.length - 1].entityChange.id;
|
||||
}
|
||||
|
||||
const ret = {
|
||||
entityChanges: syncService.getEntityChangesRecords(entityChanges),
|
||||
maxEntityChangeId: sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1')
|
||||
entityChanges: entityChangeRecords,
|
||||
lastEntityChangeId,
|
||||
outstandingPullCount: sql.getValue(`
|
||||
SELECT COUNT(id)
|
||||
FROM entity_changes
|
||||
WHERE isSynced = 1
|
||||
AND instanceId != ?
|
||||
AND id > ?`, [clientinstanceId, lastEntityChangeId])
|
||||
};
|
||||
|
||||
if (ret.entityChanges.length > 0) {
|
||||
@ -174,10 +206,10 @@ function update(req) {
|
||||
}
|
||||
}
|
||||
|
||||
const {entities} = body;
|
||||
const {entities, instanceId} = body;
|
||||
|
||||
for (const {entityChange, entity} of entities) {
|
||||
syncUpdateService.updateEntity(entityChange, entity);
|
||||
syncUpdateService.updateEntity(entityChange, entity, instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
const sourceIdService = require('../services/source_id');
|
||||
const sql = require('../services/sql');
|
||||
const attributeService = require('../services/attributes');
|
||||
const config = require('../services/config');
|
||||
@ -28,7 +27,6 @@ function index(req, res) {
|
||||
mainFontSize: parseInt(options.mainFontSize),
|
||||
treeFontSize: parseInt(options.treeFontSize),
|
||||
detailFontSize: parseInt(options.detailFontSize),
|
||||
sourceId: sourceIdService.generateSourceId(),
|
||||
maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"),
|
||||
maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"),
|
||||
instanceName: config.General ? config.General.instanceName : null,
|
||||
|
@ -4,17 +4,47 @@ const utils = require('../services/utils');
|
||||
const optionService = require('../services/options');
|
||||
const myScryptService = require('../services/my_scrypt');
|
||||
const log = require('../services/log');
|
||||
const passwordService = require("../services/password");
|
||||
|
||||
function loginPage(req, res) {
|
||||
res.render('login', { failedAuth: false });
|
||||
}
|
||||
|
||||
function login(req, res) {
|
||||
const userName = optionService.getOption('username');
|
||||
function setPasswordPage(req, res) {
|
||||
res.render('set_password', { error: false });
|
||||
}
|
||||
|
||||
function setPassword(req, res) {
|
||||
if (passwordService.isPasswordSet()) {
|
||||
return [400, "Password has been already set"];
|
||||
}
|
||||
|
||||
let {password1, password2} = req.body;
|
||||
password1 = password1.trim();
|
||||
password2 = password2.trim();
|
||||
|
||||
let error;
|
||||
|
||||
if (password1 !== password2) {
|
||||
error = "Entered passwords don't match.";
|
||||
} else if (password1.length < 4) {
|
||||
error = "Password must be at least 4 characters long.";
|
||||
}
|
||||
|
||||
if (error) {
|
||||
res.render('set_password', { error });
|
||||
return;
|
||||
}
|
||||
|
||||
passwordService.setPassword(password1);
|
||||
|
||||
res.redirect('login');
|
||||
}
|
||||
|
||||
function login(req, res) {
|
||||
const guessedPassword = req.body.password;
|
||||
|
||||
if (req.body.username === userName && verifyPassword(guessedPassword)) {
|
||||
if (verifyPassword(guessedPassword)) {
|
||||
const rememberMe = req.body.remember_me;
|
||||
|
||||
req.session.regenerate(() => {
|
||||
@ -30,7 +60,7 @@ function login(req, res) {
|
||||
}
|
||||
else {
|
||||
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy
|
||||
log.info(`WARNING: Wrong username / password from ${req.ip}, rejecting.`);
|
||||
log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
|
||||
|
||||
res.render('login', {'failedAuth': true});
|
||||
}
|
||||
@ -55,6 +85,8 @@ function logout(req, res) {
|
||||
|
||||
module.exports = {
|
||||
loginPage,
|
||||
setPasswordPage,
|
||||
setPassword,
|
||||
login,
|
||||
logout
|
||||
};
|
||||
|
@ -31,15 +31,22 @@ const scriptRoute = require('./api/script');
|
||||
const senderRoute = require('./api/sender');
|
||||
const filesRoute = require('./api/files');
|
||||
const searchRoute = require('./api/search');
|
||||
const specialNotesRoute = require('./api/special_notes.js');
|
||||
const noteMapRoute = require('./api/note_map.js');
|
||||
const specialNotesRoute = require('./api/special_notes');
|
||||
const noteMapRoute = require('./api/note_map');
|
||||
const clipperRoute = require('./api/clipper');
|
||||
const similarNotesRoute = require('./api/similar_notes');
|
||||
const keysRoute = require('./api/keys');
|
||||
const backendLogRoute = require('./api/backend_log');
|
||||
const statsRoute = require('./api/stats');
|
||||
const fontsRoute = require('./api/fonts');
|
||||
const etapiTokensApiRoutes = require('./api/etapi_tokens');
|
||||
const shareRoutes = require('../share/routes');
|
||||
const etapiAuthRoutes = require('../etapi/auth');
|
||||
const etapiAttributeRoutes = require('../etapi/attributes');
|
||||
const etapiBranchRoutes = require('../etapi/branches');
|
||||
const etapiNoteRoutes = require('../etapi/notes');
|
||||
const etapiSpecialNoteRoutes = require('../etapi/special_notes');
|
||||
const etapiSpecRoute = require('../etapi/spec');
|
||||
|
||||
const log = require('../services/log');
|
||||
const express = require('express');
|
||||
@ -51,7 +58,7 @@ const entityChangesService = require('../services/entity_changes');
|
||||
const csurf = require('csurf');
|
||||
const {createPartialContentHandler} = require("express-partial-content");
|
||||
const rateLimit = require("express-rate-limit");
|
||||
const AbstractEntity = require("../becca/entities/abstract_entity.js");
|
||||
const AbstractEntity = require("../becca/entities/abstract_entity");
|
||||
|
||||
const csrfMiddleware = csurf({
|
||||
cookie: true,
|
||||
@ -139,7 +146,7 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
|
||||
cls.namespace.bindEmitter(res);
|
||||
|
||||
const result = cls.init(() => {
|
||||
cls.set('sourceId', req.headers['trilium-source-id']);
|
||||
cls.set('componentId', req.headers['trilium-component-id']);
|
||||
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
|
||||
cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root');
|
||||
|
||||
@ -177,12 +184,13 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
|
||||
});
|
||||
}
|
||||
|
||||
const GET = 'get', POST = 'post', PUT = 'put', DELETE = 'delete';
|
||||
const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete';
|
||||
const uploadMiddleware = multer.single('upload');
|
||||
|
||||
function register(app) {
|
||||
route(GET, '/', [auth.checkAuth, csrfMiddleware], indexRoute.index);
|
||||
route(GET, '/login', [auth.checkAppInitialized], loginRoute.loginPage);
|
||||
route(GET, '/login', [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage);
|
||||
route(GET, '/set-password', [auth.checkAppInitialized], loginRoute.setPasswordPage);
|
||||
|
||||
const loginRateLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
@ -191,6 +199,7 @@ function register(app) {
|
||||
|
||||
route(POST, '/login', [loginRateLimiter], loginRoute.login);
|
||||
route(POST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout);
|
||||
route(POST, '/set-password', [auth.checkAppInitialized], loginRoute.setPassword);
|
||||
route(GET, '/setup', [], setupRoute.setupPage);
|
||||
|
||||
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
|
||||
@ -265,11 +274,11 @@ function register(app) {
|
||||
apiRoute(GET, '/api/note-map/:noteId/backlinks', noteMapRoute.getBacklinks);
|
||||
|
||||
apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote);
|
||||
apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote);
|
||||
apiRoute(GET, '/api/special-notes/week/:date', specialNotesRoute.getWeekNote);
|
||||
apiRoute(GET, '/api/special-notes/month/:month', specialNotesRoute.getMonthNote);
|
||||
apiRoute(GET, '/api/special-notes/year/:year', specialNotesRoute.getYearNote);
|
||||
apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDateNotesForMonth);
|
||||
apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote);
|
||||
apiRoute(GET, '/api/special-notes/weeks/:date', specialNotesRoute.getWeekNote);
|
||||
apiRoute(GET, '/api/special-notes/months/:month', specialNotesRoute.getMonthNote);
|
||||
apiRoute(GET, '/api/special-notes/years/:year', specialNotesRoute.getYearNote);
|
||||
apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDayNotesForMonth);
|
||||
apiRoute(POST, '/api/special-notes/sql-console', specialNotesRoute.createSqlConsole);
|
||||
apiRoute(POST, '/api/special-notes/save-sql-console', specialNotesRoute.saveSqlConsole);
|
||||
apiRoute(POST, '/api/special-notes/search-note', specialNotesRoute.createSearchNote);
|
||||
@ -288,6 +297,7 @@ function register(app) {
|
||||
apiRoute(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes);
|
||||
|
||||
apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword);
|
||||
apiRoute(POST, '/api/password/reset', passwordApiRoute.resetPassword);
|
||||
|
||||
apiRoute(POST, '/api/sync/test', syncApiRoute.testSync);
|
||||
apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow);
|
||||
@ -333,8 +343,8 @@ function register(app) {
|
||||
|
||||
// no CSRF since this is called from android app
|
||||
route(POST, '/api/sender/login', [], loginApiRoute.token, apiResultHandler);
|
||||
route(POST, '/api/sender/image', [auth.checkToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
|
||||
route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler);
|
||||
route(POST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
|
||||
route(POST, '/api/sender/note', [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler);
|
||||
|
||||
apiRoute(GET, '/api/quick-search/:searchString', searchRoute.quickSearch);
|
||||
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
|
||||
@ -350,7 +360,7 @@ function register(app) {
|
||||
route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler);
|
||||
|
||||
// in case of local electron, local calls are allowed unauthenticated, for server they need auth
|
||||
const clipperMiddleware = utils.isElectron() ? [] : [auth.checkToken];
|
||||
const clipperMiddleware = utils.isElectron() ? [] : [auth.checkEtapiToken];
|
||||
|
||||
route(GET, '/api/clipper/handshake', clipperMiddleware, clipperRoute.handshake, apiResultHandler);
|
||||
route(POST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler);
|
||||
@ -371,7 +381,19 @@ function register(app) {
|
||||
|
||||
route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
|
||||
|
||||
apiRoute(GET, '/api/etapi-tokens', etapiTokensApiRoutes.getTokens);
|
||||
apiRoute(POST, '/api/etapi-tokens', etapiTokensApiRoutes.createToken);
|
||||
apiRoute(PATCH, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.patchToken);
|
||||
apiRoute(DELETE, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.deleteToken);
|
||||
|
||||
shareRoutes.register(router);
|
||||
|
||||
etapiAuthRoutes.register(router);
|
||||
etapiAttributeRoutes.register(router);
|
||||
etapiBranchRoutes.register(router);
|
||||
etapiNoteRoutes.register(router);
|
||||
etapiSpecialNoteRoutes.register(router);
|
||||
etapiSpecRoute.register(router);
|
||||
|
||||
app.use('', router);
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 189;
|
||||
const SYNC_VERSION = 23;
|
||||
const APP_DB_VERSION = 194;
|
||||
const SYNC_VERSION = 25;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
module.exports = {
|
||||
|
@ -1,12 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('./sql');
|
||||
const etapiTokenService = require("./etapi_tokens");
|
||||
const log = require('./log');
|
||||
const sqlInit = require('./sql_init');
|
||||
const utils = require('./utils');
|
||||
const passwordEncryptionService = require('./password_encryption');
|
||||
const optionService = require('./options');
|
||||
const config = require('./config');
|
||||
const passwordService = require("./password");
|
||||
|
||||
const noAuthentication = config.General && config.General.noAuthentication === true;
|
||||
|
||||
@ -15,7 +15,11 @@ function checkAuth(req, res, next) {
|
||||
res.redirect("setup");
|
||||
}
|
||||
else if (!req.session.loggedIn && !utils.isElectron() && !noAuthentication) {
|
||||
res.redirect("login");
|
||||
if (passwordService.isPasswordSet()) {
|
||||
res.redirect("login");
|
||||
} else {
|
||||
res.redirect("set-password");
|
||||
}
|
||||
}
|
||||
else {
|
||||
next();
|
||||
@ -51,6 +55,14 @@ function checkAppInitialized(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
function checkPasswordSet(req, res, next) {
|
||||
if (!utils.isElectron() && !passwordService.isPasswordSet()) {
|
||||
res.redirect("set-password");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function checkAppNotInitialized(req, res, next) {
|
||||
if (sqlInit.isDbInitialized()) {
|
||||
reject(req, res, "App already initialized.");
|
||||
@ -60,15 +72,12 @@ function checkAppNotInitialized(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
function checkToken(req, res, next) {
|
||||
const token = req.headers.authorization;
|
||||
|
||||
// TODO: put all tokens into becca memory to avoid these requests
|
||||
if (sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
|
||||
reject(req, res, "Token not found");
|
||||
function checkEtapiToken(req, res, next) {
|
||||
if (etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
|
||||
next();
|
||||
}
|
||||
else {
|
||||
next();
|
||||
reject(req, res, "Token not found");
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,10 +96,10 @@ function checkCredentials(req, res, next) {
|
||||
const auth = new Buffer.from(header, 'base64').toString();
|
||||
const [username, password] = auth.split(/:/);
|
||||
|
||||
const dbUsername = optionService.getOption('username');
|
||||
// username is ignored
|
||||
|
||||
if (dbUsername !== username || !passwordEncryptionService.verifyPassword(password)) {
|
||||
res.status(401).send('Incorrect username and/or password');
|
||||
if (!passwordEncryptionService.verifyPassword(password)) {
|
||||
res.status(401).send('Incorrect password');
|
||||
}
|
||||
else {
|
||||
next();
|
||||
@ -101,8 +110,9 @@ module.exports = {
|
||||
checkAuth,
|
||||
checkApiAuth,
|
||||
checkAppInitialized,
|
||||
checkPasswordSet,
|
||||
checkAppNotInitialized,
|
||||
checkApiAuthOrElectron,
|
||||
checkToken,
|
||||
checkEtapiToken,
|
||||
checkCredentials
|
||||
};
|
||||
|
@ -134,18 +134,18 @@ function BackendScriptApi(currentNote, apiParams) {
|
||||
this.getNoteWithLabel = attributeService.getNoteWithLabel;
|
||||
|
||||
/**
|
||||
* If there's no branch between note and parent note, create one. Otherwise do nothing.
|
||||
* If there's no branch between note and parent note, create one. Otherwise, do nothing.
|
||||
*
|
||||
* @method
|
||||
* @param {string} noteId
|
||||
* @param {string} parentNoteId
|
||||
* @param {string} prefix - if branch will be create between note and parent note, set this prefix
|
||||
* @param {string} prefix - if branch will be created between note and parent note, set this prefix
|
||||
* @returns {void}
|
||||
*/
|
||||
this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent;
|
||||
|
||||
/**
|
||||
* If there's a branch between note and parent note, remove it. Otherwise do nothing.
|
||||
* If there's a branch between note and parent note, remove it. Otherwise, do nothing.
|
||||
*
|
||||
* @method
|
||||
* @param {string} noteId
|
||||
@ -309,8 +309,18 @@ function BackendScriptApi(currentNote, apiParams) {
|
||||
* @method
|
||||
* @param {string} date in YYYY-MM-DD format
|
||||
* @returns {Note|null}
|
||||
* @deprecated use getDayNote instead
|
||||
*/
|
||||
this.getDateNote = dateNoteService.getDateNote;
|
||||
this.getDateNote = dateNoteService.getDayNote;
|
||||
|
||||
/**
|
||||
* Returns day note for given date. If such note doesn't exist, it is created.
|
||||
*
|
||||
* @method
|
||||
* @param {string} date in YYYY-MM-DD format
|
||||
* @returns {Note|null}
|
||||
*/
|
||||
this.getDayNote = dateNoteService.getDayNote;
|
||||
|
||||
/**
|
||||
* Returns today's day note. If such note doesn't exist, it is created.
|
||||
|
@ -76,7 +76,7 @@ async function anonymize() {
|
||||
|
||||
const db = new Database(anonymizedFile);
|
||||
|
||||
db.prepare("UPDATE api_tokens SET token = 'API token value'").run();
|
||||
db.prepare("UPDATE etapi_tokens SET tokenHash = 'API token hash value'").run();
|
||||
db.prepare("UPDATE notes SET title = 'title'").run();
|
||||
db.prepare("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL").run();
|
||||
db.prepare("UPDATE note_revisions SET title = 'title'").run();
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
function getSourceId() {
|
||||
return namespace.get('sourceId');
|
||||
function getComponentId() {
|
||||
return namespace.get('componentId');
|
||||
}
|
||||
|
||||
function getLocalNowDateTime() {
|
||||
@ -80,7 +80,7 @@ module.exports = {
|
||||
set,
|
||||
namespace,
|
||||
getHoistedNoteId,
|
||||
getSourceId,
|
||||
getComponentId,
|
||||
getLocalNowDateTime,
|
||||
disableEntityEvents,
|
||||
isEntityEventsDisabled,
|
||||
|
@ -567,7 +567,7 @@ class ConsistencyChecks {
|
||||
this.runEntityChangeChecks("note_revisions", "noteRevisionId");
|
||||
this.runEntityChangeChecks("branches", "branchId");
|
||||
this.runEntityChangeChecks("attributes", "attributeId");
|
||||
this.runEntityChangeChecks("api_tokens", "apiTokenId");
|
||||
this.runEntityChangeChecks("etapi_tokens", "etapiTokenId");
|
||||
this.runEntityChangeChecks("options", "name");
|
||||
}
|
||||
|
||||
@ -660,7 +660,7 @@ class ConsistencyChecks {
|
||||
return `${tableName}: ${count}`;
|
||||
}
|
||||
|
||||
const tables = [ "notes", "note_revisions", "branches", "attributes", "api_tokens" ];
|
||||
const tables = [ "notes", "note_revisions", "branches", "attributes", "etapi_tokens" ];
|
||||
|
||||
log.info("Table counts: " + tables.map(tableName => getTableRowCount(tableName)).join(", "));
|
||||
}
|
||||
|
@ -8,9 +8,9 @@ function getEntityHashes() {
|
||||
const startTime = new Date();
|
||||
|
||||
const hashRows = sql.getRawRows(`
|
||||
SELECT entityName,
|
||||
entityId,
|
||||
hash
|
||||
SELECT entityName,
|
||||
entityId,
|
||||
hash
|
||||
FROM entity_changes
|
||||
WHERE isSynced = 1
|
||||
AND entityName != 'note_reordering'`);
|
||||
|
@ -3,7 +3,6 @@
|
||||
const noteService = require('./notes');
|
||||
const attributeService = require('./attributes');
|
||||
const dateUtils = require('./date_utils');
|
||||
const becca = require('../becca/becca');
|
||||
const sql = require('./sql');
|
||||
const protectedSessionService = require('./protected_session');
|
||||
|
||||
@ -49,7 +48,7 @@ function getRootCalendarNote() {
|
||||
}
|
||||
|
||||
/** @returns {Note} */
|
||||
function getYearNote(dateStr, rootNote) {
|
||||
function getYearNote(dateStr, rootNote = null) {
|
||||
if (!rootNote) {
|
||||
rootNote = getRootCalendarNote();
|
||||
}
|
||||
@ -88,7 +87,7 @@ function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
|
||||
}
|
||||
|
||||
/** @returns {Note} */
|
||||
function getMonthNote(dateStr, rootNote) {
|
||||
function getMonthNote(dateStr, rootNote = null) {
|
||||
if (!rootNote) {
|
||||
rootNote = getRootCalendarNote();
|
||||
}
|
||||
@ -124,7 +123,7 @@ function getMonthNote(dateStr, rootNote) {
|
||||
return monthNote;
|
||||
}
|
||||
|
||||
function getDateNoteTitle(rootNote, dayNumber, dateObj) {
|
||||
function getDayNoteTitle(rootNote, dayNumber, dateObj) {
|
||||
const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}";
|
||||
const weekDay = DAYS[dateObj.getDay()];
|
||||
|
||||
@ -137,7 +136,7 @@ function getDateNoteTitle(rootNote, dayNumber, dateObj) {
|
||||
}
|
||||
|
||||
/** @returns {Note} */
|
||||
function getDateNote(dateStr) {
|
||||
function getDayNote(dateStr) {
|
||||
dateStr = dateStr.trim().substr(0, 10);
|
||||
|
||||
let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
|
||||
@ -152,7 +151,7 @@ function getDateNote(dateStr) {
|
||||
|
||||
const dateObj = dateUtils.parseLocalDate(dateStr);
|
||||
|
||||
const noteTitle = getDateNoteTitle(rootNote, dayNumber, dateObj);
|
||||
const noteTitle = getDayNoteTitle(rootNote, dayNumber, dateObj);
|
||||
|
||||
sql.transactional(() => {
|
||||
dateNote = createNote(monthNote, noteTitle);
|
||||
@ -170,7 +169,7 @@ function getDateNote(dateStr) {
|
||||
}
|
||||
|
||||
function getTodayNote() {
|
||||
return getDateNote(dateUtils.localNowDate());
|
||||
return getDayNote(dateUtils.localNowDate());
|
||||
}
|
||||
|
||||
function getStartOfTheWeek(date, startOfTheWeek) {
|
||||
@ -197,7 +196,7 @@ function getWeekNote(dateStr, options = {}) {
|
||||
|
||||
dateStr = dateUtils.utcDateTimeStr(dateObj);
|
||||
|
||||
return getDateNote(dateStr);
|
||||
return getDayNote(dateStr);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -205,6 +204,6 @@ module.exports = {
|
||||
getYearNote,
|
||||
getMonthNote,
|
||||
getWeekNote,
|
||||
getDateNote,
|
||||
getDayNote,
|
||||
getTodayNote
|
||||
};
|
||||
|
@ -1,13 +1,19 @@
|
||||
const sql = require('./sql');
|
||||
const sourceIdService = require('./source_id');
|
||||
const dateUtils = require('./date_utils');
|
||||
const log = require('./log');
|
||||
const cls = require('./cls');
|
||||
const utils = require('./utils');
|
||||
const instanceId = require('./member_id');
|
||||
const becca = require("../becca/becca");
|
||||
|
||||
let maxEntityChangeId = 0;
|
||||
|
||||
function addEntityChangeWithinstanceId(origEntityChange, instanceId) {
|
||||
const ec = {...origEntityChange, instanceId};
|
||||
|
||||
return addEntityChange(ec);
|
||||
}
|
||||
|
||||
function addEntityChange(origEntityChange) {
|
||||
const ec = {...origEntityChange};
|
||||
|
||||
@ -17,7 +23,8 @@ function addEntityChange(origEntityChange) {
|
||||
ec.changeId = utils.randomString(12);
|
||||
}
|
||||
|
||||
ec.sourceId = ec.sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId();
|
||||
ec.componentId = ec.componentId || cls.getComponentId() || "";
|
||||
ec.instanceId = ec.instanceId || instanceId;
|
||||
ec.isSynced = ec.isSynced ? 1 : 0;
|
||||
ec.isErased = ec.isErased ? 1 : 0;
|
||||
ec.id = sql.replace("entity_changes", ec);
|
||||
@ -27,7 +34,7 @@ function addEntityChange(origEntityChange) {
|
||||
cls.addEntityChange(ec);
|
||||
}
|
||||
|
||||
function addNoteReorderingEntityChange(parentNoteId, sourceId) {
|
||||
function addNoteReorderingEntityChange(parentNoteId, componentId) {
|
||||
addEntityChange({
|
||||
entityName: "note_reordering",
|
||||
entityId: parentNoteId,
|
||||
@ -35,7 +42,8 @@ function addNoteReorderingEntityChange(parentNoteId, sourceId) {
|
||||
isErased: false,
|
||||
utcDateChanged: dateUtils.utcNowDateTime(),
|
||||
isSynced: true,
|
||||
sourceId
|
||||
componentId,
|
||||
instanceId: instanceId
|
||||
});
|
||||
|
||||
const eventService = require('./events');
|
||||
@ -129,7 +137,7 @@ function fillAllEntityChanges() {
|
||||
fillEntityChanges("note_revision_contents", "noteRevisionId");
|
||||
fillEntityChanges("recent_notes", "noteId");
|
||||
fillEntityChanges("attributes", "attributeId");
|
||||
fillEntityChanges("api_tokens", "apiTokenId");
|
||||
fillEntityChanges("etapi_tokens", "etapiTokenId");
|
||||
fillEntityChanges("options", "name", 'isSynced = 1');
|
||||
});
|
||||
}
|
||||
@ -138,6 +146,7 @@ module.exports = {
|
||||
addNoteReorderingEntityChange,
|
||||
moveEntityChangeToTop,
|
||||
addEntityChange,
|
||||
addEntityChangeWithinstanceId,
|
||||
fillAllEntityChanges,
|
||||
addEntityChangesForSector,
|
||||
getMaxEntityChangeId: () => maxEntityChangeId
|
||||
|
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 Attribute = require('../becca/entities/attribute');
|
||||
|
||||
// TODO: patch/put note content
|
||||
|
||||
function getNewNotePosition(parentNoteId) {
|
||||
const note = becca.notes[parentNoteId];
|
||||
|
||||
@ -107,6 +109,10 @@ function createNewNote(params) {
|
||||
throw new Error(`Note title must be set`);
|
||||
}
|
||||
|
||||
if (params.content === null || params.content === undefined) {
|
||||
throw new Error(`Note content must be set`);
|
||||
}
|
||||
|
||||
return sql.transactional(() => {
|
||||
const note = new Note({
|
||||
noteId: params.noteId, // optionally can force specific noteId
|
||||
@ -520,7 +526,7 @@ function updateNote(noteId, noteUpdates) {
|
||||
|
||||
/**
|
||||
* @param {Branch} branch
|
||||
* @param {string} deleteId
|
||||
* @param {string|null} deleteId
|
||||
* @param {TaskContext} taskContext
|
||||
*
|
||||
* @return {boolean} - true if note has been deleted, false otherwise
|
||||
@ -570,6 +576,17 @@ function deleteBranch(branch, deleteId, taskContext) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Note} note
|
||||
* @param {string|null} deleteId
|
||||
* @param {TaskContext} taskContext
|
||||
*/
|
||||
function deleteNote(note, deleteId, taskContext) {
|
||||
for (const branch of note.getParentBranches()) {
|
||||
deleteBranch(branch, deleteId, taskContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} noteId
|
||||
* @param {TaskContext} taskContext
|
||||
@ -915,6 +932,7 @@ module.exports = {
|
||||
createNewNoteWithTarget,
|
||||
updateNote,
|
||||
deleteBranch,
|
||||
deleteNote,
|
||||
undeleteNote,
|
||||
protectNoteRecursively,
|
||||
scanForLinks,
|
||||
|
@ -1,5 +1,5 @@
|
||||
const becca = require('../becca/becca');
|
||||
const sql = require("./sql.js");
|
||||
const sql = require("./sql");
|
||||
|
||||
function getOption(name) {
|
||||
let option;
|
||||
|
@ -1,6 +1,4 @@
|
||||
const optionService = require('./options');
|
||||
const passwordEncryptionService = require('./password_encryption');
|
||||
const myScryptService = require('./my_scrypt');
|
||||
const appInfo = require('./app_info');
|
||||
const utils = require('./utils');
|
||||
const log = require('./log');
|
||||
@ -12,21 +10,6 @@ function initDocumentOptions() {
|
||||
optionService.createOption('documentSecret', utils.randomSecureToken(16), false);
|
||||
}
|
||||
|
||||
function initSyncedOptions(username, password) {
|
||||
optionService.createOption('username', username, true);
|
||||
|
||||
optionService.createOption('passwordVerificationSalt', utils.randomSecureToken(32), true);
|
||||
optionService.createOption('passwordDerivedKeySalt', utils.randomSecureToken(32), true);
|
||||
|
||||
const passwordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(password), true);
|
||||
optionService.createOption('passwordVerificationHash', passwordVerificationKey, true);
|
||||
|
||||
// passwordEncryptionService expects these options to already exist
|
||||
optionService.createOption('encryptedDataKey', '', true);
|
||||
|
||||
passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16), true);
|
||||
}
|
||||
|
||||
function initNotSyncedOptions(initialized, opts = {}) {
|
||||
optionService.createOption('openTabs', JSON.stringify([
|
||||
{
|
||||
@ -45,7 +28,15 @@ function initNotSyncedOptions(initialized, opts = {}) {
|
||||
optionService.createOption('lastSyncedPull', '0', false);
|
||||
optionService.createOption('lastSyncedPush', '0', false);
|
||||
|
||||
optionService.createOption('theme', opts.theme || 'white', false);
|
||||
let theme = 'dark'; // default based on the poll in https://github.com/zadam/trilium/issues/2516
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const {nativeTheme} = require('electron');
|
||||
|
||||
theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
optionService.createOption('theme', theme, false);
|
||||
|
||||
optionService.createOption('syncServerHost', opts.syncServerHost || '', false);
|
||||
optionService.createOption('syncServerTimeout', '120000', false);
|
||||
@ -130,7 +121,6 @@ function getKeyboardDefaultOptions() {
|
||||
|
||||
module.exports = {
|
||||
initDocumentOptions,
|
||||
initSyncedOptions,
|
||||
initNotSyncedOptions,
|
||||
initStartupOptions
|
||||
};
|
||||
|
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) {
|
||||
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({
|
||||
|
@ -30,9 +30,9 @@ function executeBundle(bundle, apiParams = {}) {
|
||||
apiParams.startNote = bundle.note;
|
||||
}
|
||||
|
||||
const originalSourceId = cls.get('sourceId');
|
||||
const originalComponentId = cls.get('componentId');
|
||||
|
||||
cls.set('sourceId', 'script');
|
||||
cls.set('componentId', 'script');
|
||||
|
||||
// last \r\n is necessary if script contains line comment on its last line
|
||||
const script = "function() {\r\n" + bundle.script + "\r\n}";
|
||||
@ -47,7 +47,7 @@ function executeBundle(bundle, apiParams = {}) {
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
cls.set('sourceId', originalSourceId);
|
||||
cls.set('componentId', originalComponentId);
|
||||
}
|
||||
}
|
||||
|
||||
|
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