mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
ETAPI auth, spec improvements etc.
This commit is contained in:
parent
2d2641dbd7
commit
91dec23d5e
3
TODO
3
TODO
@ -1,3 +0,0 @@
|
||||
- new icon
|
||||
- polish becca entities API
|
||||
- separate private and public APIs in becca entities
|
@ -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;
|
13
db/migrations/0190__change_to_etapi_tokens.sql
Normal file
13
db/migrations/0190__change_to_etapi_tokens.sql
Normal file
@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS "etapi_tokens"
|
||||
(
|
||||
etapiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
tokenHash TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
utcDateModified TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0);
|
||||
|
||||
INSERT INTO etapi_tokens (etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified, isDeleted)
|
||||
SELECT apiTokenId, 'Trilium Sender', token, utcDateCreated, utcDateCreated, isDeleted FROM api_tokens;
|
||||
|
||||
DROP TABLE api_tokens;
|
10
db/migrations/0191__hash_tokens.js
Normal file
10
db/migrations/0191__hash_tokens.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = () => {
|
||||
const sql = require('../../src/services/sql');
|
||||
const crypto = require('crypto');
|
||||
|
||||
for (const {etapiTokenId, token} of sql.getRows("SELECT etapiTokenId, tokenHash AS token FROM etapi_tokens")) {
|
||||
const tokenHash = crypto.createHash('sha256').update(token).digest('base64');
|
||||
|
||||
sql.execute(`UPDATE etapi_tokens SET tokenHash = ? WHERE etapiTokenId = ?`, [tokenHash, etapiTokenId]);
|
||||
}
|
||||
};
|
@ -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
11464
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
const lex = require('../../src/services/search/services/lex.js');
|
||||
const lex = require('../../src/services/search/services/lex');
|
||||
|
||||
describe("Lexer fulltext", () => {
|
||||
it("simple lexing", () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
const Note = require('../../src/becca/entities/note.js');
|
||||
const Branch = require('../../src/becca/entities/branch.js');
|
||||
const Attribute = require('../../src/becca/entities/attribute.js');
|
||||
const becca = require('../../src/becca/becca.js');
|
||||
const Note = require('../../src/becca/entities/note');
|
||||
const Branch = require('../../src/becca/entities/branch');
|
||||
const Attribute = require('../../src/becca/entities/attribute');
|
||||
const becca = require('../../src/becca/becca');
|
||||
const randtoken = require('rand-token').generator({source: 'crypto'});
|
||||
|
||||
/** @returns {Note} */
|
||||
|
@ -1,4 +1,4 @@
|
||||
const handleParens = require('../../src/services/search/services/handle_parens.js');
|
||||
const handleParens = require('../../src/services/search/services/handle_parens');
|
||||
|
||||
describe("Parens handler", () => {
|
||||
it("handles parens", () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
const SearchContext = require("../../src/services/search/search_context.js");
|
||||
const parse = require('../../src/services/search/services/parse.js');
|
||||
const SearchContext = require("../../src/services/search/search_context");
|
||||
const parse = require('../../src/services/search/services/parse');
|
||||
|
||||
function tokens(toks, cur = 0) {
|
||||
return toks.map(arg => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
const searchService = require('../../src/services/search/services/search.js');
|
||||
const Note = require('../../src/becca/entities/note.js');
|
||||
const Branch = require('../../src/becca/entities/branch.js');
|
||||
const SearchContext = require('../../src/services/search/search_context.js');
|
||||
const dateUtils = require('../../src/services/date_utils.js');
|
||||
const becca = require('../../src/becca/becca.js');
|
||||
const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking.js');
|
||||
const searchService = require('../../src/services/search/services/search');
|
||||
const Note = require('../../src/becca/entities/note');
|
||||
const Branch = require('../../src/becca/entities/branch');
|
||||
const SearchContext = require('../../src/services/search/search_context');
|
||||
const dateUtils = require('../../src/services/date_utils');
|
||||
const becca = require('../../src/becca/becca');
|
||||
const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking');
|
||||
|
||||
describe("Search", () => {
|
||||
let rootNote;
|
||||
|
@ -1,7 +1,7 @@
|
||||
const {note} = require('./note_cache_mocking.js');
|
||||
const ValueExtractor = require('../../src/services/search/value_extractor.js');
|
||||
const becca = require('../../src/becca/becca.js');
|
||||
const SearchContext = require("../../src/services/search/search_context.js");
|
||||
const {note} = require('./note_cache_mocking');
|
||||
const ValueExtractor = require('../../src/services/search/value_extractor');
|
||||
const becca = require('../../src/becca/becca');
|
||||
const SearchContext = require("../../src/services/search/search_context");
|
||||
|
||||
const dsc = new SearchContext();
|
||||
|
||||
|
@ -11,7 +11,7 @@ const sessionSecret = require('./services/session_secret');
|
||||
const dataDir = require('./services/data_dir');
|
||||
const utils = require('./services/utils');
|
||||
require('./services/handlers');
|
||||
require('./becca/becca_loader.js');
|
||||
require('./becca/becca_loader');
|
||||
|
||||
const app = express();
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require("../services/sql.js");
|
||||
const sql = require("../services/sql");
|
||||
const NoteSet = require("../services/search/note_set");
|
||||
const EtapiToken = require("./entities/etapi_token");
|
||||
|
||||
/**
|
||||
* Becca is a backend cache of all notes, branches and attributes. There's a similar frontend cache Froca.
|
||||
@ -24,6 +25,8 @@ class Becca {
|
||||
this.attributeIndex = {};
|
||||
/** @type {Object.<String, Option>} */
|
||||
this.options = {};
|
||||
/** @type {Object.<String, EtapiToken>} */
|
||||
this.etapiTokens = {};
|
||||
|
||||
this.loaded = false;
|
||||
}
|
||||
@ -64,10 +67,12 @@ class Becca {
|
||||
this.dirtyNoteSetCache();
|
||||
}
|
||||
|
||||
/** @returns {Note|null} */
|
||||
getNote(noteId) {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
/** @returns {Note[]} */
|
||||
getNotes(noteIds, ignoreMissing = false) {
|
||||
const filteredNotes = [];
|
||||
|
||||
@ -88,29 +93,44 @@ class Becca {
|
||||
return filteredNotes;
|
||||
}
|
||||
|
||||
/** @returns {Branch|null} */
|
||||
getBranch(branchId) {
|
||||
return this.branches[branchId];
|
||||
}
|
||||
|
||||
/** @returns {Attribute|null} */
|
||||
getAttribute(attributeId) {
|
||||
return this.attributes[attributeId];
|
||||
}
|
||||
|
||||
/** @returns {Branch|null} */
|
||||
getBranchFromChildAndParent(childNoteId, parentNoteId) {
|
||||
return this.childParentToBranch[`${childNoteId}-${parentNoteId}`];
|
||||
}
|
||||
|
||||
/** @returns {NoteRevision|null} */
|
||||
getNoteRevision(noteRevisionId) {
|
||||
const row = sql.getRow("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [noteRevisionId]);
|
||||
|
||||
const NoteRevision = require("./entities/note_revision.js"); // avoiding circular dependency problems
|
||||
const NoteRevision = require("./entities/note_revision"); // avoiding circular dependency problems
|
||||
return row ? new NoteRevision(row) : null;
|
||||
}
|
||||
|
||||
/** @returns {Option|null} */
|
||||
getOption(name) {
|
||||
return this.options[name];
|
||||
}
|
||||
|
||||
/** @returns {EtapiToken[]} */
|
||||
getEtapiTokens() {
|
||||
return Object.values(this.etapiTokens);
|
||||
}
|
||||
|
||||
/** @returns {EtapiToken|null} */
|
||||
getEtapiToken(etapiTokenId) {
|
||||
return this.etapiTokens[etapiTokenId];
|
||||
}
|
||||
|
||||
getEntity(entityName, entityId) {
|
||||
if (!entityName || !entityId) {
|
||||
return null;
|
||||
@ -130,17 +150,19 @@ class Becca {
|
||||
return this[camelCaseEntityName][entityId];
|
||||
}
|
||||
|
||||
/** @returns {RecentNote[]} */
|
||||
getRecentNotesFromQuery(query, params = []) {
|
||||
const rows = sql.getRows(query, params);
|
||||
|
||||
const RecentNote = require("./entities/recent_note.js"); // avoiding circular dependency problems
|
||||
const RecentNote = require("./entities/recent_note"); // avoiding circular dependency problems
|
||||
return rows.map(row => new RecentNote(row));
|
||||
}
|
||||
|
||||
/** @returns {NoteRevision[]} */
|
||||
getNoteRevisionsFromQuery(query, params = []) {
|
||||
const rows = sql.getRows(query, params);
|
||||
|
||||
const NoteRevision = require("./entities/note_revision.js"); // avoiding circular dependency problems
|
||||
const NoteRevision = require("./entities/note_revision"); // avoiding circular dependency problems
|
||||
return rows.map(row => new NoteRevision(row));
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ const Note = require('./entities/note');
|
||||
const Branch = require('./entities/branch');
|
||||
const Attribute = require('./entities/attribute');
|
||||
const Option = require('./entities/option');
|
||||
const EtapiToken = require("./entities/etapi_token");
|
||||
const cls = require("../services/cls");
|
||||
const entityConstructor = require("../becca/entity_constructor");
|
||||
|
||||
@ -45,6 +46,10 @@ function load() {
|
||||
new Option(row);
|
||||
}
|
||||
|
||||
for (const row of sql.getRows(`SELECT etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified FROM etapi_tokens WHERE isDeleted = 0`)) {
|
||||
new EtapiToken(row);
|
||||
}
|
||||
|
||||
for (const noteId in becca.notes) {
|
||||
becca.notes[noteId].sortParents();
|
||||
}
|
||||
@ -75,7 +80,7 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entity
|
||||
return;
|
||||
}
|
||||
|
||||
if (["notes", "branches", "attributes"].includes(entityName)) {
|
||||
if (["notes", "branches", "attributes", "etapi_tokens"].includes(entityName)) {
|
||||
const EntityClass = entityConstructor.getEntityFromEntityName(entityName);
|
||||
const primaryKeyName = EntityClass.primaryKeyName;
|
||||
|
||||
@ -112,6 +117,8 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENT
|
||||
branchDeleted(entityId);
|
||||
} else if (entityName === 'attributes') {
|
||||
attributeDeleted(entityId);
|
||||
} else if (entityName === 'etapi_tokens') {
|
||||
etapiTokenDeleted(entityId);
|
||||
}
|
||||
});
|
||||
|
||||
@ -220,6 +227,10 @@ function noteReorderingUpdated(branchIdList) {
|
||||
}
|
||||
}
|
||||
|
||||
function etapiTokenDeleted(etapiTokenId) {
|
||||
delete becca.etapiTokens[etapiTokenId];
|
||||
}
|
||||
|
||||
eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => {
|
||||
try {
|
||||
becca.decryptProtectedNotes();
|
||||
|
@ -40,7 +40,7 @@ class AbstractEntity {
|
||||
|
||||
get becca() {
|
||||
if (!becca) {
|
||||
becca = require('../becca.js');
|
||||
becca = require('../becca');
|
||||
}
|
||||
|
||||
return becca;
|
||||
@ -116,6 +116,19 @@ class AbstractEntity {
|
||||
|
||||
eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this });
|
||||
}
|
||||
|
||||
markAsDeletedSimple() {
|
||||
const entityId = this[this.constructor.primaryKeyName];
|
||||
const entityName = this.constructor.entityName;
|
||||
|
||||
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ?
|
||||
WHERE ${this.constructor.primaryKeyName} = ?`,
|
||||
[dateUtils.utcNowDateTime(), entityId]);
|
||||
|
||||
this.addEntityChange(true);
|
||||
|
||||
eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AbstractEntity;
|
||||
|
@ -1,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;
|
@ -1,9 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
const Note = require('./note.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const sql = require("../../services/sql.js");
|
||||
const dateUtils = require("../../services/date_utils.js");
|
||||
const Note = require('./note');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
const sql = require("../../services/sql");
|
||||
const dateUtils = require("../../services/date_utils");
|
||||
const promotedAttributeDefinitionParser = require("../../services/promoted_attribute_definition_parser");
|
||||
|
||||
/**
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
const Note = require('./note.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const sql = require("../../services/sql.js");
|
||||
const dateUtils = require("../../services/date_utils.js");
|
||||
const Note = require('./note');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
const sql = require("../../services/sql");
|
||||
const dateUtils = require("../../services/date_utils");
|
||||
|
||||
/**
|
||||
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple
|
||||
|
72
src/becca/entities/etapi_token.js
Normal file
72
src/becca/entities/etapi_token.js
Normal file
@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
const sql = require("../../services/sql.js");
|
||||
|
||||
/**
|
||||
* EtapiToken is an entity representing token used to authenticate against Trilium REST API from client applications.
|
||||
* Used by:
|
||||
* - Trilium Sender
|
||||
* - ETAPI clients
|
||||
*/
|
||||
class EtapiToken extends AbstractEntity {
|
||||
static get entityName() { return "etapi_tokens"; }
|
||||
static get primaryKeyName() { return "etapiTokenId"; }
|
||||
static get hashedProperties() { return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"]; }
|
||||
|
||||
constructor(row) {
|
||||
super();
|
||||
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFromRow(row);
|
||||
this.init();
|
||||
}
|
||||
|
||||
updateFromRow(row) {
|
||||
/** @type {string} */
|
||||
this.etapiTokenId = row.etapiTokenId;
|
||||
/** @type {string} */
|
||||
this.name = row.name;
|
||||
/** @type {string} */
|
||||
this.tokenHash = row.tokenHash;
|
||||
/** @type {string} */
|
||||
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
|
||||
/** @type {string} */
|
||||
this.utcDateModified = row.utcDateModified || this.utcDateCreated;
|
||||
/** @type {boolean} */
|
||||
this.isDeleted = !!row.isDeleted;
|
||||
|
||||
this.becca.etapiTokens[this.etapiTokenId] = this;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.etapiTokenId) {
|
||||
this.becca.etapiTokens[this.etapiTokenId] = this;
|
||||
}
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
etapiTokenId: this.etapiTokenId,
|
||||
name: this.name,
|
||||
tokenHash: this.tokenHash,
|
||||
utcDateCreated: this.utcDateCreated,
|
||||
utcDateModified: this.utcDateModified,
|
||||
isDeleted: this.isDeleted
|
||||
}
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
this.becca.etapiTokens[this.etapiTokenId] = this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EtapiToken;
|
@ -6,8 +6,8 @@ const sql = require('../../services/sql');
|
||||
const utils = require('../../services/utils');
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const entityChangesService = require('../../services/entity_changes');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const NoteRevision = require("./note_revision.js");
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
const NoteRevision = require("./note_revision");
|
||||
|
||||
const LABEL = 'label';
|
||||
const RELATION = 'relation';
|
||||
@ -984,7 +984,7 @@ class Note extends AbstractEntity {
|
||||
}
|
||||
}
|
||||
else {
|
||||
const Attribute = require("./attribute.js");
|
||||
const Attribute = require("./attribute");
|
||||
|
||||
new Attribute({
|
||||
noteId: this.noteId,
|
||||
@ -1016,7 +1016,7 @@ class Note extends AbstractEntity {
|
||||
* @return {Attribute}
|
||||
*/
|
||||
addAttribute(type, name, value = "", isInheritable = false, position = 1000) {
|
||||
const Attribute = require("./attribute.js");
|
||||
const Attribute = require("./attribute");
|
||||
|
||||
return new Attribute({
|
||||
noteId: this.noteId,
|
||||
|
@ -4,9 +4,9 @@ const protectedSessionService = require('../../services/protected_session');
|
||||
const utils = require('../../services/utils');
|
||||
const sql = require('../../services/sql');
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const becca = require('../becca.js');
|
||||
const becca = require('../becca');
|
||||
const entityChangesService = require('../../services/entity_changes');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
|
||||
/**
|
||||
* NoteRevision represents snapshot of note's title and content at some point in the past.
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const dateUtils = require('../../services/date_utils.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
|
||||
/**
|
||||
* Option represents name-value pair, either directly configurable by the user or some system property.
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const dateUtils = require('../../services/date_utils.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const AbstractEntity = require("./abstract_entity");
|
||||
|
||||
/**
|
||||
* RecentNote represents recently visited note.
|
||||
|
@ -3,7 +3,7 @@ const NoteRevision = require('./entities/note_revision');
|
||||
const Branch = require('./entities/branch');
|
||||
const Attribute = require('./entities/attribute');
|
||||
const RecentNote = require('./entities/recent_note');
|
||||
const ApiToken = require('./entities/api_token');
|
||||
const EtapiToken = require('./entities/etapi_token');
|
||||
const Option = require('./entities/option');
|
||||
|
||||
const ENTITY_NAME_TO_ENTITY = {
|
||||
@ -14,7 +14,7 @@ const ENTITY_NAME_TO_ENTITY = {
|
||||
"note_revisions": NoteRevision,
|
||||
"note_revision_contents": NoteRevision,
|
||||
"recent_notes": RecentNote,
|
||||
"api_tokens": ApiToken,
|
||||
"etapi_tokens": EtapiToken,
|
||||
"options": Option
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
const becca = require('./becca.js');
|
||||
const becca = require('./becca');
|
||||
const log = require('../services/log');
|
||||
const beccaService = require('./becca_service.js');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
|
@ -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
43
src/etapi/auth.js
Normal file
@ -0,0 +1,43 @@
|
||||
const becca = require("../becca/becca");
|
||||
const eu = require("./etapi_utils");
|
||||
const passwordEncryptionService = require("../services/password_encryption.js");
|
||||
const etapiTokenService = require("../services/etapi_tokens.js");
|
||||
|
||||
function register(router) {
|
||||
eu.NOT_AUTHENTICATED_ROUTE(router, 'post', '/etapi/auth/login', (req, res, next) => {
|
||||
const {password, tokenName} = req.body;
|
||||
|
||||
if (!passwordEncryptionService.verifyPassword(password)) {
|
||||
throw new eu.EtapiError(401, "WRONG_PASSWORD", "Wrong password.");
|
||||
}
|
||||
|
||||
const {authToken} = etapiTokenService.createToken(tokenName || "ETAPI login");
|
||||
|
||||
res.json({
|
||||
authToken
|
||||
});
|
||||
});
|
||||
|
||||
eu.route(router, 'post', '/etapi/auth/logout', (req, res, next) => {
|
||||
const parsed = etapiTokenService.parseAuthToken(req.headers.authorization);
|
||||
|
||||
if (!parsed || !parsed.etapiTokenId) {
|
||||
throw new eu.EtapiError(400, eu.GENERIC_CODE, "Cannot logout this token.");
|
||||
}
|
||||
|
||||
const etapiToken = becca.getEtapiToken(parsed.etapiTokenId);
|
||||
|
||||
if (!etapiToken) {
|
||||
// shouldn't happen since this already passed auth validation
|
||||
throw new Error(`Cannot find the token ${parsed.etapiTokenId}.`);
|
||||
}
|
||||
|
||||
etapiToken.markAsDeletedSimple();
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register
|
||||
}
|
@ -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");
|
||||
|
||||
|
@ -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}'
|
||||
|
@ -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
|
||||
}
|
@ -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;
|
||||
|
@ -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)) {
|
||||
|
@ -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'),
|
||||
|
128
src/public/app/dialogs/options/etapi.js
Normal file
128
src/public/app/dialogs/options/etapi.js
Normal file
@ -0,0 +1,128 @@
|
||||
import server from "../../services/server.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = `
|
||||
<h4>ETAPI</h4>
|
||||
|
||||
<p>ETAPI is a REST API used to access Trilium instance programmatically, without UI. <br/>
|
||||
See more details on <a href="https://github.com/zadam/trilium/wiki/ETAPI">wiki</a> and <a onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">ETAPI OpenAPI spec</a>.</p>
|
||||
|
||||
<button type="button" class="btn btn-sm" id="create-etapi-token">Create new ETAPI token</button>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<h5>Existing tokens</h5>
|
||||
|
||||
<div id="no-tokens-yet">There are no tokens yet. Click on the button above to create one.</div>
|
||||
|
||||
<div style="overflow: auto; height: 500px;">
|
||||
<table id="tokens-table" class="table table-stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Token name</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.token-table-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: 3px;
|
||||
margin-right: 20px;
|
||||
font-size: large;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.token-table-button:hover {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
export default class EtapiOptions {
|
||||
constructor() {
|
||||
$("#options-etapi").html(TPL);
|
||||
|
||||
$("#create-etapi-token").on("click", async () => {
|
||||
const promptDialog = await import('../../dialogs/prompt.js');
|
||||
const tokenName = await promptDialog.ask({
|
||||
title: "New ETAPI token",
|
||||
message: "Please enter new token's name",
|
||||
defaultValue: "new token"
|
||||
});
|
||||
|
||||
if (!tokenName.trim()) {
|
||||
alert("Token name can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
const {token} = await server.post('etapi-tokens', {tokenName});
|
||||
|
||||
await promptDialog.ask({
|
||||
title: "ETAPI token created",
|
||||
message: 'Copy the created token into clipboard. Trilium stores the token hashed and this is the last time you see it.',
|
||||
defaultValue: token
|
||||
});
|
||||
|
||||
this.refreshTokens();
|
||||
});
|
||||
|
||||
this.refreshTokens();
|
||||
}
|
||||
|
||||
async refreshTokens() {
|
||||
const $noTokensYet = $("#no-tokens-yet");
|
||||
const $tokensTable = $("#tokens-table");
|
||||
|
||||
const tokens = await server.get('etapi-tokens');
|
||||
|
||||
$noTokensYet.toggle(tokens.length === 0);
|
||||
$tokensTable.toggle(tokens.length > 0);
|
||||
|
||||
const $tokensTableBody = $tokensTable.find("tbody");
|
||||
$tokensTableBody.empty();
|
||||
|
||||
for (const token of tokens) {
|
||||
$tokensTableBody.append(
|
||||
$("<tr>")
|
||||
.append($("<td>").text(token.name))
|
||||
.append($("<td>").text(token.utcDateCreated))
|
||||
.append($("<td>").append(
|
||||
$('<span class="bx bx-pen token-table-button" title="Rename this token"></span>')
|
||||
.on("click", () => this.renameToken(token.etapiTokenId, token.name)),
|
||||
$('<span class="bx bx-trash token-table-button" title="Delete / deactive this token"></span>')
|
||||
.on("click", () => this.deleteToken(token.etapiTokenId, token.name))
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async renameToken(etapiTokenId, oldName) {
|
||||
const promptDialog = await import('../../dialogs/prompt.js');
|
||||
const tokenName = await promptDialog.ask({
|
||||
title: "Rename token",
|
||||
message: "Please enter new token's name",
|
||||
defaultValue: oldName
|
||||
});
|
||||
|
||||
await server.patch(`etapi-tokens/${etapiTokenId}`, {name: tokenName});
|
||||
|
||||
this.refreshTokens();
|
||||
}
|
||||
|
||||
async deleteToken(etapiTokenId, name) {
|
||||
if (!confirm(`Are you sure you want to delete ETAPI token "${name}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await server.remove(`etapi-tokens/${etapiTokenId}`);
|
||||
|
||||
this.refreshTokens();
|
||||
}
|
||||
}
|
@ -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; });
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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,
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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!
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
30
src/routes/api/etapi_tokens.js
Normal file
30
src/routes/api/etapi_tokens.js
Normal file
@ -0,0 +1,30 @@
|
||||
const etapiTokenService = require("../../services/etapi_tokens");
|
||||
|
||||
function getTokens() {
|
||||
const tokens = etapiTokenService.getTokens();
|
||||
|
||||
tokens.sort((a, b) => a.utcDateCreated < b.utcDateCreated ? -1 : 1);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function createToken(req) {
|
||||
return {
|
||||
authToken: etapiTokenService.createToken(req.body.tokenName)
|
||||
};
|
||||
}
|
||||
|
||||
function patchToken(req) {
|
||||
etapiTokenService.renameToken(req.params.etapiTokenId, req.body.name);
|
||||
}
|
||||
|
||||
function deleteToken(req) {
|
||||
etapiTokenService.deleteToken(req.params.etapiTokenId);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTokens,
|
||||
createToken,
|
||||
patchToken,
|
||||
deleteToken
|
||||
};
|
@ -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 = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
const passwordService = require('../../services/password.js');
|
||||
const passwordService = require('../../services/password');
|
||||
|
||||
function changePassword(req) {
|
||||
if (passwordService.isPasswordSet()) {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 });
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -309,8 +309,18 @@ function BackendScriptApi(currentNote, apiParams) {
|
||||
* @method
|
||||
* @param {string} date in YYYY-MM-DD format
|
||||
* @returns {Note|null}
|
||||
* @deprecated use getDayNote instead
|
||||
*/
|
||||
this.getDateNote = dateNoteService.getDateNote;
|
||||
this.getDateNote = dateNoteService.getDayNote;
|
||||
|
||||
/**
|
||||
* Returns day note for given date. If such note doesn't exist, it is created.
|
||||
*
|
||||
* @method
|
||||
* @param {string} date in YYYY-MM-DD format
|
||||
* @returns {Note|null}
|
||||
*/
|
||||
this.getDayNote = dateNoteService.getDayNote;
|
||||
|
||||
/**
|
||||
* Returns today's day note. If such note doesn't exist, it is created.
|
||||
|
@ -76,7 +76,7 @@ async function anonymize() {
|
||||
|
||||
const db = new Database(anonymizedFile);
|
||||
|
||||
db.prepare("UPDATE api_tokens SET token = 'API token value'").run();
|
||||
db.prepare("UPDATE etapi_tokens SET tokenHash = 'API token hash value'").run();
|
||||
db.prepare("UPDATE notes SET title = 'title'").run();
|
||||
db.prepare("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL").run();
|
||||
db.prepare("UPDATE note_revisions SET title = 'title'").run();
|
||||
|
@ -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(", "));
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
107
src/services/etapi_tokens.js
Normal file
107
src/services/etapi_tokens.js
Normal file
@ -0,0 +1,107 @@
|
||||
const becca = require("../becca/becca");
|
||||
const utils = require("./utils");
|
||||
const EtapiToken = require("../becca/entities/etapi_token");
|
||||
const crypto = require("crypto");
|
||||
|
||||
function getTokens() {
|
||||
return becca.getEtapiTokens();
|
||||
}
|
||||
|
||||
function getTokenHash(token) {
|
||||
return crypto.createHash('sha256').update(token).digest('base64');
|
||||
}
|
||||
|
||||
function createToken(tokenName) {
|
||||
const token = utils.randomSecureToken();
|
||||
const tokenHash = getTokenHash(token);
|
||||
|
||||
const etapiToken = new EtapiToken({
|
||||
name: tokenName,
|
||||
tokenHash
|
||||
}).save();
|
||||
|
||||
return {
|
||||
authToken: `${etapiToken.etapiTokenId}_${token}`
|
||||
};
|
||||
}
|
||||
|
||||
function parseAuthToken(auth) {
|
||||
if (!auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chunks = auth.split("_");
|
||||
|
||||
if (chunks.length === 1) {
|
||||
return { token: auth }; // legacy format without etapiTokenId
|
||||
}
|
||||
else if (chunks.length === 2) {
|
||||
return {
|
||||
etapiTokenId: chunks[0],
|
||||
token: chunks[1]
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null; // wrong format
|
||||
}
|
||||
}
|
||||
|
||||
function isValidAuthHeader(auth) {
|
||||
const parsed = parseAuthToken(auth);
|
||||
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const authTokenHash = getTokenHash(parsed.token);
|
||||
|
||||
if (parsed.etapiTokenId) {
|
||||
const etapiToken = becca.getEtapiToken(parsed.etapiTokenId);
|
||||
|
||||
if (!etapiToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return etapiToken.tokenHash === authTokenHash;
|
||||
}
|
||||
else {
|
||||
for (const etapiToken of becca.getEtapiTokens()) {
|
||||
if (etapiToken.tokenHash === authTokenHash) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function renameToken(etapiTokenId, newName) {
|
||||
const etapiToken = becca.getEtapiToken(etapiTokenId);
|
||||
|
||||
if (!etapiToken) {
|
||||
throw new Error(`Token ${etapiTokenId} does not exist`);
|
||||
}
|
||||
|
||||
etapiToken.name = newName;
|
||||
etapiToken.save();
|
||||
}
|
||||
|
||||
function deleteToken(etapiTokenId) {
|
||||
const etapiToken = becca.getEtapiToken(etapiTokenId);
|
||||
|
||||
if (!etapiToken) {
|
||||
return; // ok, already deleted
|
||||
}
|
||||
|
||||
etapiToken.isDeleted = true;
|
||||
etapiToken.save();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTokens,
|
||||
createToken,
|
||||
renameToken,
|
||||
deleteToken,
|
||||
parseAuthToken,
|
||||
isValidAuthHeader
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
const becca = require('../becca/becca');
|
||||
const sql = require("./sql.js");
|
||||
const sql = require("./sql");
|
||||
|
||||
function getOption(name) {
|
||||
let option;
|
||||
|
@ -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 = "";
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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');
|
||||
|
@ -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>
|
||||
|
@ -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">×</span>
|
||||
|
2
src/www
2
src/www
@ -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
12
test-etapi/_login.http
Normal 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);
|
||||
%}
|
@ -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() {
|
||||
|
@ -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");
|
||||
|
@ -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"); %}
|
||||
|
@ -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");
|
||||
|
@ -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
34
test-etapi/logout.http
Normal 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
103
test-etapi/no-token.http
Normal 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); %}
|
@ -1,4 +1,5 @@
|
||||
POST {{triliumHost}}/etapi/refresh-note-ordering/root
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.test("Request executed successfully", function() {
|
||||
|
@ -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
|
||||
|
||||
{
|
||||
|
@ -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
|
||||
|
||||
{
|
||||
|
@ -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
|
||||
|
||||
{
|
||||
|
@ -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"); %}
|
||||
|
@ -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);
|
||||
%}
|
||||
|
Loading…
x
Reference in New Issue
Block a user