ETAPI auth, spec improvements etc.

This commit is contained in:
zadam 2022-01-10 17:09:20 +01:00
parent 2d2641dbd7
commit 91dec23d5e
90 changed files with 1468 additions and 11753 deletions

3
TODO
View File

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

View File

@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS "mig_api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
token TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0);
INSERT INTO mig_api_tokens (apiTokenId, name, token, utcDateCreated, isDeleted)
SELECT apiTokenId, 'Trilium Sender', token, utcDateCreated, isDeleted FROM api_tokens;
DROP TABLE api_tokens;
ALTER TABLE mig_api_tokens RENAME TO api_tokens;

View File

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

View File

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

View File

@ -10,11 +10,11 @@ CREATE TABLE IF NOT EXISTS "entity_changes" (
`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,
etapiTokenId TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
token TEXT NOT NULL,
tokenHash TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0);
CREATE TABLE IF NOT EXISTS "branches" (

11464
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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} */

View File

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

View File

@ -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 => {

View File

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

View File

@ -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();

View File

@ -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();

View File

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

View File

@ -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();

View File

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

View File

@ -1,40 +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.
* Used by:
* - Trilium Sender
* - ETAPI clients
*/
class ApiToken extends AbstractEntity {
static get entityName() { return "api_tokens"; }
static get primaryKeyName() { return "apiTokenId"; }
static get hashedProperties() { return ["apiTokenId", "name", "token", "utcDateCreated"]; }
constructor(row) {
super();
/** @type {string} */
this.apiTokenId = row.apiTokenId;
/** @type {string} */
this.name = row.name;
/** @type {string} */
this.token = row.token;
/** @type {string} */
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
}
getPojo() {
return {
apiTokenId: this.apiTokenId,
name: this.name,
token: this.token,
utcDateCreated: this.utcDateCreated
}
}
}
module.exports = ApiToken;

View File

@ -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");
/**

View File

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

View File

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

View File

@ -6,8 +6,8 @@ const sql = require('../../services/sql');
const utils = require('../../services/utils');
const 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,

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

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

View File

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

View File

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

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

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

View File

@ -1,24 +1,24 @@
const becca = require("../becca/becca.js");
const ru = require("./route_utils");
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.js");
const validators = require("./validators");
function register(router) {
ru.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => {
const branch = ru.getAndCheckBranch(req.params.branchId);
eu.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
res.json(mappers.mapBranchToPojo(branch));
});
ru.route(router, 'post' ,'/etapi/branches', (req, res, next) => {
eu.route(router, 'post' ,'/etapi/branches', (req, res, next) => {
const params = req.body;
ru.getAndCheckNote(params.noteId);
ru.getAndCheckNote(params.parentNoteId);
eu.getAndCheckNote(params.noteId);
eu.getAndCheckNote(params.parentNoteId);
const existing = becca.getBranchFromChildAndParent(params.noteId, params.parentNoteId);
@ -36,7 +36,7 @@ function register(router) {
res.json(mappers.mapBranchToPojo(branch));
}
catch (e) {
throw new ru.EtapiError(400, ru.GENERIC_CODE, e.message);
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
}
});
@ -46,15 +46,15 @@ function register(router) {
'isExpanded': validators.isBoolean
};
ru.route(router, 'patch' ,'/etapi/branches/:branchId', (req, res, next) => {
const branch = ru.getAndCheckBranch(req.params.branchId);
eu.route(router, 'patch' ,'/etapi/branches/:branchId', (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId);
ru.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
res.json(mappers.mapBranchToPojo(branch));
});
ru.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => {
eu.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => {
const branch = becca.getBranch(req.params.branchId);
if (!branch || branch.isDeleted) {
@ -66,8 +66,8 @@ function register(router) {
res.sendStatus(204);
});
ru.route(router, 'post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
ru.getAndCheckNote(req.params.parentNoteId);
eu.route(router, 'post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
eu.getAndCheckNote(req.params.parentNoteId);
entityChangesService.addNoteReorderingEntityChange(req.params.parentNoteId, "etapi");

View File

@ -13,6 +13,8 @@ info:
servers:
- url: http://localhost:37740/etapi
- url: http://localhost:8080/etapi
security:
- EtapiTokenAuth: []
paths:
/create-note:
post:
@ -43,16 +45,142 @@ paths:
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
parameters:
- name: noteId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
responses:
'200':
description: note response
@ -69,12 +197,6 @@ paths:
patch:
description: patch a note identified by the noteId with changes in the body
operationId: patchNoteById
parameters:
- name: noteId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
requestBody:
required: true
content:
@ -97,13 +219,6 @@ paths:
delete:
description: deletes a single note based on the noteId supplied
operationId: deleteNoteById
parameters:
- name: noteId
in: path
description: noteId of note to delete
required: true
schema:
$ref: '#/components/schemas/EntityId'
responses:
'204':
description: note deleted
@ -114,15 +229,15 @@ paths:
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
parameters:
- name: branchId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
responses:
'200':
description: branch response
@ -161,12 +276,6 @@ paths:
patch:
description: patch a branch identified by the branchId with changes in the body
operationId: patchBranchById
parameters:
- name: branchId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
requestBody:
required: true
content:
@ -187,15 +296,10 @@ paths:
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.
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
parameters:
- name: branchId
in: path
description: branchId of note to delete
required: true
schema:
$ref: '#/components/schemas/EntityId'
responses:
'204':
description: branch deleted
@ -206,15 +310,15 @@ paths:
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
parameters:
- name: attributeId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
responses:
'200':
description: attribute response
@ -253,12 +357,6 @@ paths:
patch:
description: patch a attribute identified by the attributeId with changes in the body
operationId: patchAttributeById
parameters:
- name: attributeId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
requestBody:
required: true
content:
@ -281,13 +379,6 @@ paths:
delete:
description: deletes a attribute based on the attributeId supplied.
operationId: deleteAttributeById
parameters:
- name: attributeId
in: path
description: attributeId of attribute to delete
required: true
schema:
$ref: '#/components/schemas/EntityId'
responses:
'204':
description: attribute deleted
@ -298,8 +389,17 @@ paths:
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.
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':
@ -310,29 +410,230 @@ paths:
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:
noteId:
$ref: '#/components/schemas/EntityId'
description: Leave this out unless you want to force a specific noteId
branchId:
$ref: '#/components/schemas/EntityId'
description: Leave this out unless you want to force a specific branchId
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:
@ -438,6 +739,18 @@ components:
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}'

View File

@ -1,9 +1,13 @@
const cls = require("../services/cls.js");
const sql = require("../services/sql.js");
const log = require("../services/log.js");
const becca = require("../becca/becca.js");
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();
@ -26,40 +30,44 @@ function sendError(res, statusCode, code, message) {
}
function checkEtapiAuth(req, res, next) {
if (false) {
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
next();
}
else {
next();
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) => {
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
router[method](path, checkEtapiAuth, (req, res, next) => processRequest(req, res, routeHandler, next, method, path));
}
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 NOT_AUTHENTICATED_ROUTE(router, method, path, routeHandler) {
router[method](path, (req, res, next) => processRequest(req, res, routeHandler, next, method, path));
}
function getAndCheckNote(noteId) {
@ -121,12 +129,11 @@ function validateAndPatch(entity, props, allowedProperties) {
module.exports = {
EtapiError,
sendError,
checkEtapiAuth,
route,
NOT_AUTHENTICATED_ROUTE,
GENERIC_CODE,
validateAndPatch,
getAndCheckNote,
getAndCheckBranch,
getAndCheckAttribute,
getNotAllowedPatchPropertyError: (propertyName, allowedProperties) => new EtapiError(400, "PROPERTY_NOT_ALLOWED_FOR_PATCH", `Property '${propertyName}' is not allowed to be patched, allowed properties are ${allowedProperties}.`),
getAndCheckAttribute
}

View File

@ -1,36 +1,48 @@
const becca = require("../becca/becca");
const utils = require("../services/utils");
const ru = require("./route_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) {
ru.route(router, 'get', '/etapi/notes', (req, res, next) => {
eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
const {search} = req.query;
if (!search?.trim()) {
throw new ru.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory");
throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory");
}
const searchParams = parseSearchParams(req);
const foundNotes = searchService.searchNotes(search, searchParams);
res.json(foundNotes.map(note => mappers.mapNoteToPojo(note)));
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);
});
ru.route(router, 'get', '/etapi/notes/:noteId', (req, res, next) => {
const note = ru.getAndCheckNote(req.params.noteId);
eu.route(router, 'get', '/etapi/notes/:noteId', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'post' ,'/etapi/create-note', (req, res, next) => {
eu.route(router, 'post' ,'/etapi/create-note', (req, res, next) => {
const params = req.body;
ru.getAndCheckNote(params.parentNoteId);
eu.getAndCheckNote(params.parentNoteId);
try {
const resp = noteService.createNewNote(params);
@ -41,7 +53,7 @@ function register(router) {
});
}
catch (e) {
return ru.sendError(res, 400, ru.GENERIC_CODE, e.message);
return eu.sendError(res, 400, eu.GENERIC_CODE, e.message);
}
});
@ -51,19 +63,19 @@ function register(router) {
'mime': validators.isString
};
ru.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => {
const note = ru.getAndCheckNote(req.params.noteId)
eu.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId)
if (note.isProtected) {
throw new ru.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI`);
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI`);
}
ru.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'delete' ,'/etapi/notes/:noteId', (req, res, next) => {
eu.route(router, 'delete' ,'/etapi/notes/:noteId', (req, res, next) => {
const {noteId} = req.params;
const note = becca.getNote(noteId);
@ -77,8 +89,8 @@ function register(router) {
res.sendStatus(204);
});
ru.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = ru.getAndCheckNote(req.params.noteId);
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);
@ -90,8 +102,8 @@ function register(router) {
res.send(note.getContent());
});
ru.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = ru.getAndCheckNote(req.params.noteId);
eu.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
note.setContent(req.body);
@ -130,7 +142,7 @@ function parseBoolean(obj, name) {
}
if (!['true', 'false'].includes(obj[name])) {
throw new ru.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'`);
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';
@ -144,7 +156,7 @@ function parseInteger(obj, name) {
const integer = parseInt(obj[name]);
if (!['asc', 'desc'].includes(obj[name])) {
throw new ru.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'`);
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'`);
}
return integer;
@ -158,7 +170,7 @@ function parseOrderDirection(obj, name) {
const integer = parseInt(obj[name]);
if (Number.isNaN(integer)) {
throw new ru.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse integer '${name}' value '${obj[name]}`);
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse integer '${name}' value '${obj[name]}`);
}
return integer;

View File

@ -1,11 +1,11 @@
const specialNotesService = require("../services/special_notes");
const dateNotesService = require("../services/date_notes");
const ru = require("./route_utils");
const eu = require("./etapi_utils");
const mappers = require("./mappers");
const getDateInvalidError = date => new ru.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
const getMonthInvalidError = month => new ru.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
const getYearInvalidError = year => new ru.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
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)) {
@ -16,7 +16,7 @@ function isValidDate(date) {
}
function register(router) {
ru.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
eu.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
@ -27,18 +27,18 @@ function register(router) {
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'get', '/etapi/date/:date', (req, res, next) => {
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.getDateNote(date);
const note = dateNotesService.getDayNote(date);
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'get', '/etapi/week/:date', (req, res, next) => {
eu.route(router, 'get', '/etapi/calendar/weeks/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
@ -49,7 +49,7 @@ function register(router) {
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'get', '/etapi/month/:month', (req, res, next) => {
eu.route(router, 'get', '/etapi/calendar/months/:month', (req, res, next) => {
const {month} = req.params;
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
@ -60,7 +60,7 @@ function register(router) {
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'get', '/etapi/year/:year', (req, res, next) => {
eu.route(router, 'get', '/etapi/calendar/years/:year', (req, res, next) => {
const {year} = req.params;
if (!/[0-9]{4}/.test(year)) {

View File

@ -15,6 +15,7 @@ export async function showDialog(openTab) {
import('./options/shortcuts.js'),
import('./options/code_notes.js'),
import('./options/password.js'),
import('./options/etapi.js'),
import('./options/backup.js'),
import('./options/sync.js'),
import('./options/other.js'),

View File

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

View File

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

View File

@ -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());

View File

@ -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,

View File

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

View File

@ -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"

View File

@ -41,6 +41,10 @@ async function put(url, data, componentId) {
return await call('PUT', url, data, {'trilium-component-id': componentId});
}
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});
}
@ -185,6 +189,7 @@ export default {
get,
post,
put,
patch,
remove,
ajax,
// don't remove, used from CKEditor image upload!

View File

@ -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();

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
const becca = require("../../becca/becca");
const utils = require("../../services/utils");
const noteService = require("../../services/notes");
const attributeService = require("../../services/attributes");
const Branch = require("../../becca/entities/branch");
const specialNotesService = require("../../services/special_notes");
const dateNotesService = require("../../services/date_notes");
const entityChangesService = require("../../services/entity_changes.js");
const TaskContext = require("../../services/task_context.js");
function register(router) {
}
module.exports = {
register
}

View File

@ -0,0 +1,30 @@
const etapiTokenService = require("../../services/etapi_tokens");
function getTokens() {
const tokens = etapiTokenService.getTokens();
tokens.sort((a, b) => a.utcDateCreated < b.utcDateCreated ? -1 : 1);
return tokens;
}
function createToken(req) {
return {
authToken: etapiTokenService.createToken(req.body.tokenName)
};
}
function patchToken(req) {
etapiTokenService.renameToken(req.params.etapiTokenId, req.body.name);
}
function deleteToken(req) {
etapiTokenService.deleteToken(req.params.etapiTokenId);
}
module.exports = {
getTokens,
createToken,
patchToken,
deleteToken
};

View File

@ -10,8 +10,8 @@ const appInfo = require('../../services/app_info');
const eventService = require('../../services/events');
const sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql');
const ApiToken = require('../../becca/entities/api_token');
const ws = require("../../services/ws");
const etapiTokenService = require("../../services/etapi_tokens");
function loginSync(req) {
if (!sqlInit.schemaExists()) {
@ -90,15 +90,12 @@ function token(req) {
return [401, "Incorrect password"];
}
const apiToken = new ApiToken({
// for backwards compatibility with Sender which does not send the name
name: req.body.tokenName || "Trilium Sender",
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 = {

View File

@ -1,6 +1,6 @@
"use strict";
const passwordService = require('../../services/password.js');
const passwordService = require('../../services/password');
function changePassword(req) {
if (passwordService.isPasswordSet()) {

View File

@ -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,

View File

@ -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,

View File

@ -4,7 +4,7 @@ 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.js");
const passwordService = require("../services/password");
function loginPage(req, res) {
res.render('login', { failedAuth: false });

View File

@ -31,15 +31,17 @@ 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');
@ -56,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,
@ -182,7 +184,7 @@ 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) {
@ -272,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);
@ -341,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);
@ -358,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);
@ -379,7 +381,14 @@ 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);

View File

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

View File

@ -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 config = require('./config');
const passwordService = require("./password.js");
const passwordService = require("./password");
const noAuthentication = config.General && config.General.noAuthentication === true;
@ -72,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");
}
}
@ -116,6 +113,6 @@ module.exports = {
checkPasswordSet,
checkAppNotInitialized,
checkApiAuthOrElectron,
checkToken,
checkEtapiToken,
checkCredentials
};

View File

@ -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.

View File

@ -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();

View File

@ -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(", "));
}

View File

@ -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');
@ -124,7 +123,7 @@ function getMonthNote(dateStr, rootNote = null) {
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
};

View File

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

View File

@ -0,0 +1,107 @@
const becca = require("../becca/becca");
const utils = require("./utils");
const EtapiToken = require("../becca/entities/etapi_token");
const crypto = require("crypto");
function getTokens() {
return becca.getEtapiTokens();
}
function getTokenHash(token) {
return crypto.createHash('sha256').update(token).digest('base64');
}
function createToken(tokenName) {
const token = utils.randomSecureToken();
const tokenHash = getTokenHash(token);
const etapiToken = new EtapiToken({
name: tokenName,
tokenHash
}).save();
return {
authToken: `${etapiToken.etapiTokenId}_${token}`
};
}
function parseAuthToken(auth) {
if (!auth) {
return null;
}
const chunks = auth.split("_");
if (chunks.length === 1) {
return { token: auth }; // legacy format without etapiTokenId
}
else if (chunks.length === 2) {
return {
etapiTokenId: chunks[0],
token: chunks[1]
}
}
else {
return null; // wrong format
}
}
function isValidAuthHeader(auth) {
const parsed = parseAuthToken(auth);
if (!parsed) {
return false;
}
const authTokenHash = getTokenHash(parsed.token);
if (parsed.etapiTokenId) {
const etapiToken = becca.getEtapiToken(parsed.etapiTokenId);
if (!etapiToken) {
return false;
}
return etapiToken.tokenHash === authTokenHash;
}
else {
for (const etapiToken of becca.getEtapiTokens()) {
if (etapiToken.tokenHash === authTokenHash) {
return true;
}
}
return false;
}
}
function renameToken(etapiTokenId, newName) {
const etapiToken = becca.getEtapiToken(etapiTokenId);
if (!etapiToken) {
throw new Error(`Token ${etapiTokenId} does not exist`);
}
etapiToken.name = newName;
etapiToken.save();
}
function deleteToken(etapiTokenId) {
const etapiToken = becca.getEtapiToken(etapiTokenId);
if (!etapiToken) {
return; // ok, already deleted
}
etapiToken.isDeleted = true;
etapiToken.save();
}
module.exports = {
getTokens,
createToken,
renameToken,
deleteToken,
parseAuthToken,
isValidAuthHeader
};

View File

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

View File

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

View File

@ -11,7 +11,7 @@ const RelationWhereExp = require('../expressions/relation_where');
const PropertyComparisonExp = require('../expressions/property_comparison');
const AttributeExistsExp = require('../expressions/attribute_exists');
const LabelComparisonExp = require('../expressions/label_comparison');
const NoteFlatTextExp = require('../expressions/note_flat_text.js');
const NoteFlatTextExp = require('../expressions/note_flat_text');
const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext');
const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit');

View File

@ -124,9 +124,13 @@ function parseQueryToExpression(query, searchContext) {
});
if (searchContext.debug) {
log.info(`Fulltext tokens: ` + JSON.stringify(fulltextTokens));
log.info(`Expression tokens: ` + JSON.stringify(structuredExpressionTokens, null, 4));
log.info("Expression tree: " + JSON.stringify(expression, null, 4));
searchContext.debugInfo = {
fulltextTokens,
structuredExpressionTokens,
expression
};
log.info("Search debug: " + JSON.stringify(searchContext.debugInfo, null, 4));
}
return expression;

View File

@ -23,7 +23,7 @@ function getInboxNote(date) {
}
else {
inbox = attributeService.getNoteWithLabel('inbox')
|| dateNoteService.getDateNote(date);
|| dateNoteService.getDayNote(date);
}
return inbox;
@ -137,7 +137,7 @@ function saveSqlConsole(sqlConsoleNoteId) {
const sqlConsoleHome =
attributeService.getNoteWithLabel('sqlConsoleHome')
|| dateNoteService.getDateNote(today);
|| dateNoteService.getDayNote(today);
const result = sqlConsoleNote.cloneTo(sqlConsoleHome.noteId);
@ -179,7 +179,7 @@ function getSearchHome() {
const today = dateUtils.localNowDate();
return hoistedNote.searchNoteInSubtree('#searchHome')
|| dateNoteService.getDateNote(today);
|| dateNoteService.getDayNote(today);
}
}

View File

@ -1,7 +1,6 @@
const { Menu, Tray } = require('electron');
const path = require('path');
const windowService = require("./window.js");
const {getMainWindow} = require("./window.js");
const windowService = require("./window");
const UPDATE_TRAY_EVENTS = [
'minimize', 'maximize', 'show', 'hide'
@ -81,7 +80,7 @@ const updateTrayMenu = () => {
tray?.setContextMenu(contextMenu);
}
const changeVisibility = () => {
const window = getMainWindow();
const window = windowService.getMainWindow();
if (isVisible) {
window.hide();

View File

@ -7,7 +7,7 @@ const config = require('./config');
const syncMutexService = require('./sync_mutex');
const protectedSessionService = require('./protected_session');
const becca = require("../becca/becca");
const AbstractEntity = require("../becca/entities/abstract_entity.js");
const AbstractEntity = require("../becca/entities/abstract_entity");
let webSocketServer;
let lastSyncedPush = null;
@ -139,7 +139,7 @@ function fillInAdditionalProperties(entityChange) {
// entities with higher number can reference the entities with lower number
const ORDERING = {
"api_tokens": 0,
"etapi_tokens": 0,
"attributes": 1,
"branches": 1,
"note_contents": 1,

View File

@ -1,7 +1,7 @@
const shaca = require("./shaca/shaca");
const shacaLoader = require("./shaca/shaca_loader");
const shareRoot = require("./share_root");
const contentRenderer = require("./content_renderer.js");
const contentRenderer = require("./content_renderer");
function getSharedSubTreeRoot(note) {
if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {

View File

@ -1,7 +1,7 @@
"use strict";
const sql = require('../sql');
const shaca = require('./shaca.js');
const shaca = require('./shaca');
const log = require('../../services/log');
const Note = require('./entities/note');
const Branch = require('./entities/branch');

View File

@ -22,6 +22,9 @@
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-password">Password</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-etapi">ETAPI</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-backup">Backup</a>
</li>
@ -41,6 +44,7 @@
<div id="options-shortcuts" class="tab-pane"></div>
<div id="options-code-notes" class="tab-pane"></div>
<div id="options-password" class="tab-pane"></div>
<div id="options-etapi" class="tab-pane"></div>
<div id="options-backup" class="tab-pane"></div>
<div id="options-sync-setup" class="tab-pane"></div>
<div id="options-other" class="tab-pane"></div>

View File

@ -3,7 +3,7 @@
<div class="modal-content">
<form id="prompt-dialog-form">
<div class="modal-header">
<h5 class="modal-title mr-auto">Prompt</h5>
<h5 class="modal-title mr-auto" id="prompt-title">Prompt</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>

View File

@ -23,7 +23,7 @@ const https = require('https');
const config = require('./services/config');
const log = require('./services/log');
const appInfo = require('./services/app_info');
const ws = require('./services/ws.js');
const ws = require('./services/ws');
const utils = require('./services/utils');
const sqlInit = require('./services/sql_init');
const port = require('./services/port');

12
test-etapi/_login.http Normal file
View File

@ -0,0 +1,12 @@
POST {{triliumHost}}/etapi/auth/login
Content-Type: application/json
{
"password": "1234"
}
> {%
client.assert(response.status === 200, "Response status is not 200");
client.global.set("authToken", response.body.authToken);
%}

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
@ -24,6 +25,7 @@ Content-Type: application/json
### Clone to another location
POST {{triliumHost}}/etapi/branches
Authorization: {{authToken}}
Content-Type: application/json
{
@ -45,6 +47,7 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {
@ -60,6 +63,7 @@ GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {
@ -71,6 +75,7 @@ GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {
@ -83,6 +88,7 @@ GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {
@ -96,6 +102,7 @@ GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
POST {{triliumHost}}/etapi/attributes
Content-Type: application/json
Authorization: {{authToken}}
{
"noteId": "{{createdNoteId}}",
@ -118,6 +125,7 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
@ -16,6 +17,7 @@ Content-Type: application/json
###
POST {{triliumHost}}/etapi/attributes
Authorization: {{authToken}}
Content-Type: application/json
{
@ -31,30 +33,35 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 204, "Response status is not 204"); %}
### repeat the DELETE request to test the idempotency
DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 204, "Response status is not 204"); %}
###
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
> {%
client.assert(response.status === 404, "Response status is not 404");

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
@ -16,6 +17,7 @@ Content-Type: application/json
### Clone to another location
POST {{triliumHost}}/etapi/branches
Authorization: {{authToken}}
Content-Type: application/json
{
@ -28,36 +30,42 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 204, "Response status is not 204"); %}
### repeat the DELETE request to test the idempotency
DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 204, "Response status is not 204"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
> {%
client.assert(response.status === 404, "Response status is not 404");
@ -67,11 +75,13 @@ GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
@ -16,6 +17,7 @@ Content-Type: application/json
###
POST {{triliumHost}}/etapi/attributes
Authorization: {{authToken}}
Content-Type: application/json
{
@ -31,6 +33,7 @@ Content-Type: application/json
### Clone to another location
POST {{triliumHost}}/etapi/branches
Authorization: {{authToken}}
Content-Type: application/json
{
@ -43,42 +46,49 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 204, "Response status is not 204"); %}
### repeat the DELETE request to test the idempotency
DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {% client.assert(response.status === 204, "Response status is not 204"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
> {%
client.assert(response.status === 404, "Response status is not 404");
@ -88,6 +98,7 @@ GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
Authorization: {{authToken}}
> {%
client.assert(response.status === 404, "Response status is not 404");
@ -97,6 +108,7 @@ GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {%
client.assert(response.status === 404, "Response status is not 404");
@ -106,6 +118,7 @@ GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
###
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
> {%
client.assert(response.status === 404, "Response status is not 404");

View File

@ -1,4 +1,5 @@
GET {{triliumHost}}/etapi/inbox/2022-01-01
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {
@ -8,7 +9,8 @@ GET {{triliumHost}}/etapi/inbox/2022-01-01
###
GET {{triliumHost}}/etapi/date/2022-01-01
GET {{triliumHost}}/etapi/calendar/days/2022-01-01
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {
@ -18,18 +20,20 @@ GET {{triliumHost}}/etapi/date/2022-01-01
###
GET {{triliumHost}}/etapi/date/2022-1
GET {{triliumHost}}/etapi/calendar/days/2022-1
Authorization: {{authToken}}
> {%
client.test("Correct error handling", function() {
client.assert(response.status === 400, "Response status is not 400");
client.assert(response.body.code == "DATE_INVALID");
client.assert(response.body.code === "DATE_INVALID");
});
%}
###
GET {{triliumHost}}/etapi/week/2022-01-01
GET {{triliumHost}}/etapi/calendar/weeks/2022-01-01
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {
@ -39,18 +43,20 @@ GET {{triliumHost}}/etapi/week/2022-01-01
###
GET {{triliumHost}}/etapi/week/2022-1
GET {{triliumHost}}/etapi/calendar/weeks/2022-1
Authorization: {{authToken}}
> {%
client.test("Correct error handling", function() {
client.assert(response.status === 400, "Response status is not 400");
client.assert(response.body.code == "DATE_INVALID");
client.assert(response.body.code === "DATE_INVALID");
});
%}
###
GET {{triliumHost}}/etapi/month/2022-01
GET {{triliumHost}}/etapi/calendar/months/2022-01
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {
@ -60,18 +66,20 @@ GET {{triliumHost}}/etapi/month/2022-01
###
GET {{triliumHost}}/etapi/month/2022-1
GET {{triliumHost}}/etapi/calendar/months/2022-1
Authorization: {{authToken}}
> {%
client.test("Correct error handling", function() {
client.assert(response.status === 400, "Response status is not 400");
client.assert(response.body.code == "MONTH_INVALID");
client.assert(response.body.code === "MONTH_INVALID");
});
%}
###
GET {{triliumHost}}/etapi/year/2022
GET {{triliumHost}}/etapi/calendar/years/2022
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {
@ -81,12 +89,13 @@ GET {{triliumHost}}/etapi/year/2022
###
GET {{triliumHost}}/etapi/year/202
GET {{triliumHost}}/etapi/calendar/years/202
Authorization: {{authToken}}
> {%
client.test("Correct error handling", function() {
client.assert(response.status === 400, "Response status is not 400");
client.assert(response.body.code == "YEAR_INVALID");
client.assert(response.body.code === "YEAR_INVALID");
});
%}

34
test-etapi/logout.http Normal file
View File

@ -0,0 +1,34 @@
POST {{triliumHost}}/etapi/auth/login
Content-Type: application/json
{
"password": "1234"
}
> {%
client.assert(response.status === 200);
client.global.set("testAuthToken", response.body.authToken);
%}
###
GET {{triliumHost}}/etapi/notes/root
Authorization: {{testAuthToken}}
> {% client.assert(response.status === 200); %}
###
POST {{triliumHost}}/etapi/auth/logout
Authorization: {{testAuthToken}}
Content-Type: application/json
> {% client.assert(response.status === 204); %}
###
GET {{triliumHost}}/etapi/notes/root
Authorization: {{testAuthToken}}
> {% client.assert(response.status === 401); %}

103
test-etapi/no-token.http Normal file
View File

@ -0,0 +1,103 @@
GET {{triliumHost}}/etapi/notes?search=aaa
> {% client.assert(response.status === 401); %}
###
GET {{triliumHost}}/etapi/notes/root
> {% client.assert(response.status === 401); %}
###
PATCH {{triliumHost}}/etapi/notes/root
Authorization: fakeauth
> {% client.assert(response.status === 401); %}
###
DELETE {{triliumHost}}/etapi/notes/root
Authorization: fakeauth
> {% client.assert(response.status === 401); %}
###
GET {{triliumHost}}/etapi/branches/root
Authorization: fakeauth
> {% client.assert(response.status === 401); %}
###
PATCH {{triliumHost}}/etapi/branches/root
> {% client.assert(response.status === 401); %}
###
DELETE {{triliumHost}}/etapi/branches/root
> {% client.assert(response.status === 401); %}
###
GET {{triliumHost}}/etapi/attributes/000
> {% client.assert(response.status === 401); %}
###
PATCH {{triliumHost}}/etapi/attributes/000
> {% client.assert(response.status === 401); %}
###
DELETE {{triliumHost}}/etapi/attributes/000
> {% client.assert(response.status === 401); %}
###
GET {{triliumHost}}/etapi/inbox/2022-02-22
> {% client.assert(response.status === 401); %}
###
GET {{triliumHost}}/etapi/calendar/days/2022-02-22
Authorization: fakeauth
> {% client.assert(response.status === 401); %}
###
GET {{triliumHost}}/etapi/calendar/weeks/2022-02-22
> {% client.assert(response.status === 401); %}
###
GET {{triliumHost}}/etapi/calendar/months/2022-02
> {% client.assert(response.status === 401); %}
###
GET {{triliumHost}}/etapi/calendar/years/2022
> {% client.assert(response.status === 401); %}
###
POST {{triliumHost}}/etapi/create-note
> {% client.assert(response.status === 401); %}
### Fake URL will get a 404 even without token
GET {{triliumHost}}/etapi/zzzzzz
> {% client.assert(response.status === 404); %}

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/refresh-note-ordering/root
Authorization: {{authToken}}
> {%
client.test("Request executed successfully", function() {

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
@ -16,6 +17,7 @@ Content-Type: application/json
###
POST {{triliumHost}}/etapi/attributes
Authorization: {{authToken}}
Content-Type: application/json
{
@ -31,6 +33,7 @@ Content-Type: application/json
###
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
Content-Type: application/json
{
@ -40,6 +43,7 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
> {%
client.assert(response.body.value === "CHANGED");
@ -48,6 +52,7 @@ client.assert(response.body.value === "CHANGED");
###
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
Content-Type: application/json
{
@ -62,6 +67,7 @@ Content-Type: application/json
###
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Authorization: {{authToken}}
Content-Type: application/json
{

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
@ -13,6 +14,7 @@ Content-Type: application/json
###
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
Content-Type: application/json
{
@ -24,6 +26,7 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
> {%
client.assert(response.status === 200);
@ -35,6 +38,7 @@ client.assert(response.body.isExpanded === true);
###
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
Content-Type: application/json
{
@ -49,6 +53,7 @@ Content-Type: application/json
###
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
Authorization: {{authToken}}
Content-Type: application/json
{

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
@ -14,6 +15,7 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {%
client.assert(response.status === 200);
@ -25,6 +27,7 @@ client.assert(response.body.mime === 'application/json');
###
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
Content-Type: application/json
{
@ -36,6 +39,7 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
> {%
client.assert(response.status === 200);
@ -47,6 +51,7 @@ client.assert(response.body.mime === 'text/html');
###
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
Content-Type: application/json
{
@ -61,6 +66,7 @@ Content-Type: application/json
###
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
Authorization: {{authToken}}
Content-Type: application/json
{

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
@ -14,6 +15,7 @@ Content-Type: application/json
###
PUT {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
Authorization: {{authToken}}
Content-Type: text/plain
Changed content
@ -21,5 +23,6 @@ Changed content
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
Authorization: {{authToken}}
> {% client.assert(response.body === "Changed content"); %}

View File

@ -1,4 +1,5 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
@ -13,23 +14,26 @@ Content-Type: application/json
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
Authorization: {{authToken}}
> {% client.global.set("content", response.body); %}
###
GET {{triliumHost}}/etapi/notes?search={{content}}
GET {{triliumHost}}/etapi/notes?search={{content}}&debug=true
Authorization: {{authToken}}
> {%
client.assert(response.status === 200);
client.assert(response.body.length === 1);
client.assert(response.body.results.length === 1);
%}
### Same but with fast search which doesn't look in the content so 0 notes should be found
GET {{triliumHost}}/etapi/notes?search={{content}}&fastSearch=true
Authorization: {{authToken}}
> {%
client.assert(response.status === 200);
client.assert(response.body.length === 0);
client.assert(response.body.results.length === 0);
%}