mirror of
https://github.com/zadam/trilium.git
synced 2025-11-05 04:45:47 +01:00
moved all sources to src directory
This commit is contained in:
15
src/routes/api/anonymization.js
Normal file
15
src/routes/api/anonymization.js
Normal file
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const anonymization = require('../../services/anonymization');
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.post('/anonymize', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await anonymization.anonymize();
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
13
src/routes/api/app_info.js
Normal file
13
src/routes/api/app_info.js
Normal file
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const app_info = require('../../services/app_info');
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
res.send(app_info);
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
48
src/routes/api/attributes.js
Normal file
48
src/routes/api/attributes.js
Normal file
@@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const auth = require('../../services/auth');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const utils = require('../../services/utils');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
res.send(await sql.getAll("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
|
||||
}));
|
||||
|
||||
router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
const attributes = req.body;
|
||||
const now = utils.nowDate();
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
for (const attr of attributes) {
|
||||
if (attr.attributeId) {
|
||||
await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ? WHERE attributeId = ?",
|
||||
[attr.name, attr.value, now, attr.attributeId]);
|
||||
}
|
||||
else {
|
||||
attr.attributeId = utils.newAttributeId();
|
||||
|
||||
await sql.insert("attributes", {
|
||||
attributeId: attr.attributeId,
|
||||
noteId: noteId,
|
||||
name: attr.name,
|
||||
value: attr.value,
|
||||
dateCreated: now,
|
||||
dateModified: now
|
||||
});
|
||||
}
|
||||
|
||||
await sync_table.addAttributeSync(attr.attributeId);
|
||||
}
|
||||
});
|
||||
|
||||
res.send(await sql.getAll("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
83
src/routes/api/cleanup.js
Normal file
83
src/routes/api/cleanup.js
Normal file
@@ -0,0 +1,83 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const utils = require('../../services/utils');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const auth = require('../../services/auth');
|
||||
const log = require('../../services/log');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.post('/cleanup-soft-deleted-items', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await sql.doInTransaction(async () => {
|
||||
const noteIdsToDelete = await sql.getFirstColumn("SELECT noteId FROM notes WHERE isDeleted = 1");
|
||||
const noteIdsSql = noteIdsToDelete
|
||||
.map(noteId => "'" + utils.sanitizeSql(noteId) + "'")
|
||||
.join(', ');
|
||||
|
||||
await sql.execute(`DELETE FROM event_log WHERE noteId IN (${noteIdsSql})`);
|
||||
|
||||
await sql.execute(`DELETE FROM note_revisions WHERE noteId IN (${noteIdsSql})`);
|
||||
|
||||
await sql.execute(`DELETE FROM note_images WHERE noteId IN (${noteIdsSql})`);
|
||||
|
||||
await sql.execute(`DELETE FROM attributes WHERE noteId IN (${noteIdsSql})`);
|
||||
|
||||
await sql.execute("DELETE FROM note_tree WHERE isDeleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM note_images WHERE isDeleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM images WHERE isDeleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM notes WHERE isDeleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM recent_notes");
|
||||
|
||||
await sync_table.cleanupSyncRowsForMissingEntities("notes", "noteId");
|
||||
await sync_table.cleanupSyncRowsForMissingEntities("note_tree", "noteTreeId");
|
||||
await sync_table.cleanupSyncRowsForMissingEntities("note_revisions", "noteRevisionId");
|
||||
await sync_table.cleanupSyncRowsForMissingEntities("recent_notes", "noteTreeId");
|
||||
|
||||
log.info("Following notes has been completely cleaned from database: " + noteIdsSql);
|
||||
});
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.post('/cleanup-unused-images', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
const unusedImageIds = await sql.getFirstColumn(`
|
||||
SELECT images.imageId
|
||||
FROM images
|
||||
LEFT JOIN note_images ON note_images.imageId = images.imageId AND note_images.isDeleted = 0
|
||||
WHERE
|
||||
images.isDeleted = 0
|
||||
AND note_images.noteImageId IS NULL`);
|
||||
|
||||
const now = utils.nowDate();
|
||||
|
||||
for (const imageId of unusedImageIds) {
|
||||
log.info(`Deleting unused image: ${imageId}`);
|
||||
|
||||
await sql.execute("UPDATE images SET isDeleted = 1, data = null, dateModified = ? WHERE imageId = ?",
|
||||
[now, imageId]);
|
||||
|
||||
await sync_table.addImageSync(imageId, sourceId);
|
||||
}
|
||||
});
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.post('/vacuum-database', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await sql.execute("VACUUM");
|
||||
|
||||
log.info("Database has been vacuumed.");
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
84
src/routes/api/cloning.js
Normal file
84
src/routes/api/cloning.js
Normal file
@@ -0,0 +1,84 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const auth = require('../../services/auth');
|
||||
const utils = require('../../services/utils');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
const tree = require('../../services/tree');
|
||||
|
||||
router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const parentNoteId = req.params.parentNoteId;
|
||||
const childNoteId = req.params.childNoteId;
|
||||
const prefix = req.body.prefix;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
if (!await tree.validateParentChild(res, parentNoteId, childNoteId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(notePosition) FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0', [parentNoteId]);
|
||||
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
const noteTree = {
|
||||
noteTreeId: utils.newNoteTreeId(),
|
||||
noteId: childNoteId,
|
||||
parentNoteId: parentNoteId,
|
||||
prefix: prefix,
|
||||
notePosition: newNotePos,
|
||||
isExpanded: 0,
|
||||
dateModified: utils.nowDate(),
|
||||
isDeleted: 0
|
||||
};
|
||||
|
||||
await sql.replace("note_tree", noteTree);
|
||||
|
||||
await sync_table.addNoteTreeSync(noteTree.noteTreeId, sourceId);
|
||||
|
||||
await sql.execute("UPDATE note_tree SET isExpanded = 1 WHERE noteId = ?", [parentNoteId]);
|
||||
});
|
||||
|
||||
res.send({ success: true });
|
||||
}));
|
||||
|
||||
router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
const afterNoteTreeId = req.params.afterNoteTreeId;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
const afterNote = await tree.getNoteTree(afterNoteTreeId);
|
||||
|
||||
if (!await tree.validateParentChild(res, afterNote.parentNoteId, noteId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
// we don't change dateModified so other changes are prioritized in case of conflict
|
||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||
await sql.execute("UPDATE note_tree SET notePosition = notePosition + 1 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
|
||||
[afterNote.parentNoteId, afterNote.notePosition]);
|
||||
|
||||
await sync_table.addNoteReorderingSync(afterNote.parentNoteId, sourceId);
|
||||
|
||||
const noteTree = {
|
||||
noteTreeId: utils.newNoteTreeId(),
|
||||
noteId: noteId,
|
||||
parentNoteId: afterNote.parentNoteId,
|
||||
notePosition: afterNote.notePosition + 1,
|
||||
isExpanded: 0,
|
||||
dateModified: utils.nowDate(),
|
||||
isDeleted: 0
|
||||
};
|
||||
|
||||
await sql.replace("note_tree", noteTree);
|
||||
|
||||
await sync_table.addNoteTreeSync(noteTree.noteTreeId, sourceId);
|
||||
});
|
||||
|
||||
res.send({ success: true });
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
27
src/routes/api/event_log.js
Normal file
27
src/routes/api/event_log.js
Normal file
@@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await deleteOld();
|
||||
|
||||
const result = await sql.getAll("SELECT * FROM event_log ORDER BY dateAdded DESC");
|
||||
|
||||
res.send(result);
|
||||
}));
|
||||
|
||||
async function deleteOld() {
|
||||
const cutoffId = await sql.getFirstValue("SELECT id FROM event_log ORDER BY id DESC LIMIT 1000, 1");
|
||||
|
||||
if (cutoffId) {
|
||||
await sql.doInTransaction(async () => {
|
||||
await sql.execute("DELETE FROM event_log WHERE id < ?", [cutoffId]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
57
src/routes/api/export.js
Normal file
57
src/routes/api/export.js
Normal file
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const rimraf = require('rimraf');
|
||||
const fs = require('fs');
|
||||
const sql = require('../../services/sql');
|
||||
const data_dir = require('../../services/data_dir');
|
||||
const html = require('html');
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
||||
|
||||
if (!fs.existsSync(data_dir.EXPORT_DIR)) {
|
||||
fs.mkdirSync(data_dir.EXPORT_DIR);
|
||||
}
|
||||
|
||||
const completeExportDir = data_dir.EXPORT_DIR + '/' + directory;
|
||||
|
||||
if (fs.existsSync(completeExportDir)) {
|
||||
rimraf.sync(completeExportDir);
|
||||
}
|
||||
|
||||
fs.mkdirSync(completeExportDir);
|
||||
|
||||
const noteTreeId = await sql.getFirstValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]);
|
||||
|
||||
await exportNote(noteTreeId, completeExportDir);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
async function exportNote(noteTreeId, dir) {
|
||||
const noteTree = await sql.getFirst("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]);
|
||||
const note = await sql.getFirst("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]);
|
||||
|
||||
const pos = (noteTree.notePosition + '').padStart(4, '0');
|
||||
|
||||
fs.writeFileSync(dir + '/' + pos + '-' + note.title + '.html', html.prettyPrint(note.content, {indent_size: 2}));
|
||||
|
||||
const children = await sql.getAll("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]);
|
||||
|
||||
if (children.length > 0) {
|
||||
const childrenDir = dir + '/' + pos + '-' + note.title;
|
||||
|
||||
fs.mkdirSync(childrenDir);
|
||||
|
||||
for (const child of children) {
|
||||
await exportNote(child.noteTreeId, childrenDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
148
src/routes/api/image.js
Normal file
148
src/routes/api/image.js
Normal file
@@ -0,0 +1,148 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const auth = require('../../services/auth');
|
||||
const utils = require('../../services/utils');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const multer = require('multer')();
|
||||
const imagemin = require('imagemin');
|
||||
const imageminMozJpeg = require('imagemin-mozjpeg');
|
||||
const imageminPngQuant = require('imagemin-pngquant');
|
||||
const imageminGifLossy = require('imagemin-giflossy');
|
||||
const jimp = require('jimp');
|
||||
const imageType = require('image-type');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
|
||||
const fs = require('fs');
|
||||
|
||||
router.get('/:imageId/:filename', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
|
||||
const image = await sql.getFirst("SELECT * FROM images WHERE imageId = ?", [req.params.imageId]);
|
||||
|
||||
if (!image) {
|
||||
return res.status(404).send({});
|
||||
}
|
||||
else if (image.data === null) {
|
||||
res.set('Content-Type', 'image/png');
|
||||
return res.send(fs.readFileSync(RESOURCE_DIR + '/db/image-deleted.png'));
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'image/' + image.format);
|
||||
|
||||
res.send(image.data);
|
||||
}));
|
||||
|
||||
router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
|
||||
const sourceId = req.headers.source_id;
|
||||
const noteId = req.query.noteId;
|
||||
const file = req.file;
|
||||
|
||||
const note = await sql.getFirst("SELECT * FROM notes WHERE noteId = ?", [noteId]);
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).send(`Note ${noteId} doesn't exist.`);
|
||||
}
|
||||
|
||||
if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) {
|
||||
return res.status(400).send("Unknown image type: " + file.mimetype);
|
||||
}
|
||||
|
||||
const now = utils.nowDate();
|
||||
|
||||
const resizedImage = await resize(file.buffer);
|
||||
const optimizedImage = await optimize(resizedImage);
|
||||
|
||||
const imageFormat = imageType(optimizedImage);
|
||||
|
||||
const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, "");
|
||||
const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext);
|
||||
|
||||
const imageId = utils.newImageId();
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await sql.insert("images", {
|
||||
imageId: imageId,
|
||||
format: imageFormat.ext,
|
||||
name: fileName,
|
||||
checksum: utils.hash(optimizedImage),
|
||||
data: optimizedImage,
|
||||
isDeleted: 0,
|
||||
dateModified: now,
|
||||
dateCreated: now
|
||||
});
|
||||
|
||||
await sync_table.addImageSync(imageId, sourceId);
|
||||
|
||||
const noteImageId = utils.newNoteImageId();
|
||||
|
||||
await sql.insert("note_images", {
|
||||
noteImageId: noteImageId,
|
||||
noteId: noteId,
|
||||
imageId: imageId,
|
||||
isDeleted: 0,
|
||||
dateModified: now,
|
||||
dateCreated: now
|
||||
});
|
||||
|
||||
await sync_table.addNoteImageSync(noteImageId, sourceId);
|
||||
});
|
||||
|
||||
res.send({
|
||||
uploaded: true,
|
||||
url: `/api/images/${imageId}/${fileName}`
|
||||
});
|
||||
}));
|
||||
|
||||
const MAX_SIZE = 1000;
|
||||
const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs
|
||||
|
||||
async function resize(buffer) {
|
||||
const image = await jimp.read(buffer);
|
||||
|
||||
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) {
|
||||
image.resize(MAX_SIZE, jimp.AUTO);
|
||||
}
|
||||
else if (image.bitmap.height > MAX_SIZE) {
|
||||
image.resize(jimp.AUTO, MAX_SIZE);
|
||||
}
|
||||
else if (buffer.byteLength <= MAX_BYTE_SIZE) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// we do resizing with max quality which will be trimmed during optimization step next
|
||||
image.quality(100);
|
||||
|
||||
// when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background
|
||||
image.background(0xFFFFFFFF);
|
||||
|
||||
// getBuffer doesn't support promises so this workaround
|
||||
return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve(data);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async function optimize(buffer) {
|
||||
return await imagemin.buffer(buffer, {
|
||||
plugins: [
|
||||
imageminMozJpeg({
|
||||
quality: 50
|
||||
}),
|
||||
imageminPngQuant({
|
||||
quality: "0-70"
|
||||
}),
|
||||
imageminGifLossy({
|
||||
lossy: 80,
|
||||
optimize: '3' // needs to be string
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
107
src/routes/api/import.js
Normal file
107
src/routes/api/import.js
Normal file
@@ -0,0 +1,107 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const fs = require('fs');
|
||||
const sql = require('../../services/sql');
|
||||
const data_dir = require('../../services/data_dir');
|
||||
const utils = require('../../services/utils');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
||||
const parentNoteId = req.params.parentNoteId;
|
||||
|
||||
const dir = data_dir.EXPORT_DIR + '/' + directory;
|
||||
|
||||
await sql.doInTransaction(async () => await importNotes(dir, parentNoteId));
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
async function importNotes(dir, parentNoteId) {
|
||||
const parent = await sql.getFirst("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
|
||||
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileList = fs.readdirSync(dir);
|
||||
|
||||
for (const file of fileList) {
|
||||
const path = dir + '/' + file;
|
||||
|
||||
if (fs.lstatSync(path).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!file.endsWith('.html')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileNameWithoutExt = file.substr(0, file.length - 5);
|
||||
|
||||
let noteTitle;
|
||||
let notePos;
|
||||
|
||||
const match = fileNameWithoutExt.match(/^([0-9]{4})-(.*)$/);
|
||||
if (match) {
|
||||
notePos = parseInt(match[1]);
|
||||
noteTitle = match[2];
|
||||
}
|
||||
else {
|
||||
let maxPos = await sql.getFirstValue("SELECT MAX(notePosition) FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]);
|
||||
if (maxPos) {
|
||||
notePos = maxPos + 1;
|
||||
}
|
||||
else {
|
||||
notePos = 0;
|
||||
}
|
||||
|
||||
noteTitle = fileNameWithoutExt;
|
||||
}
|
||||
|
||||
const noteText = fs.readFileSync(path, "utf8");
|
||||
|
||||
const noteId = utils.newNoteId();
|
||||
const noteTreeId = utils.newnoteRevisionId();
|
||||
|
||||
const now = utils.nowDate();
|
||||
|
||||
await sql.insert('note_tree', {
|
||||
noteTreeId: noteTreeId,
|
||||
noteId: noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
notePosition: notePos,
|
||||
isExpanded: 0,
|
||||
isDeleted: 0,
|
||||
dateModified: now
|
||||
});
|
||||
|
||||
await sync_table.addNoteTreeSync(noteTreeId);
|
||||
|
||||
await sql.insert('notes', {
|
||||
noteId: noteId,
|
||||
title: noteTitle,
|
||||
content: noteText,
|
||||
isDeleted: 0,
|
||||
isProtected: 0,
|
||||
type: 'text',
|
||||
mime: 'text/html',
|
||||
dateCreated: now,
|
||||
dateModified: now
|
||||
});
|
||||
|
||||
await sync_table.addNoteSync(noteId);
|
||||
|
||||
const noteDir = dir + '/' + fileNameWithoutExt;
|
||||
|
||||
if (fs.existsSync(noteDir) && fs.lstatSync(noteDir).isDirectory()) {
|
||||
await importNotes(noteDir, noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
73
src/routes/api/login.js
Normal file
73
src/routes/api/login.js
Normal file
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const options = require('../../services/options');
|
||||
const utils = require('../../services/utils');
|
||||
const source_id = require('../../services/source_id');
|
||||
const auth = require('../../services/auth');
|
||||
const password_encryption = require('../../services/password_encryption');
|
||||
const protected_session = require('../../services/protected_session');
|
||||
const app_info = require('../../services/app_info');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.post('/sync', wrap(async (req, res, next) => {
|
||||
const timestampStr = req.body.timestamp;
|
||||
|
||||
const timestamp = utils.parseDate(timestampStr);
|
||||
|
||||
const now = new Date();
|
||||
|
||||
if (Math.abs(timestamp.getTime() - now.getTime()) > 5000) {
|
||||
res.status(400);
|
||||
res.send({ message: 'Auth request time is out of sync' });
|
||||
}
|
||||
|
||||
const dbVersion = req.body.dbVersion;
|
||||
|
||||
if (dbVersion !== app_info.db_version) {
|
||||
res.status(400);
|
||||
res.send({ message: 'Non-matching db versions, local is version ' + app_info.db_version });
|
||||
}
|
||||
|
||||
const documentSecret = await options.getOption('document_secret');
|
||||
const expectedHash = utils.hmac(documentSecret, timestampStr);
|
||||
|
||||
const givenHash = req.body.hash;
|
||||
|
||||
if (expectedHash !== givenHash) {
|
||||
res.status(400);
|
||||
res.send({ message: "Sync login hash doesn't match" });
|
||||
}
|
||||
|
||||
req.session.loggedIn = true;
|
||||
|
||||
res.send({
|
||||
sourceId: source_id.getCurrentSourceId()
|
||||
});
|
||||
}));
|
||||
|
||||
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
||||
router.post('/protected', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const password = req.body.password;
|
||||
|
||||
if (!await password_encryption.verifyPassword(password)) {
|
||||
res.send({
|
||||
success: false,
|
||||
message: "Given current password doesn't match hash"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const decryptedDataKey = await password_encryption.getDataKey(password);
|
||||
|
||||
const protectedSessionId = protected_session.setDataKey(req, decryptedDataKey);
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
protectedSessionId: protectedSessionId
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
26
src/routes/api/migration.js
Normal file
26
src/routes/api/migration.js
Normal file
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../../services/auth');
|
||||
const options = require('../../services/options');
|
||||
const migration = require('../../services/migration');
|
||||
const app_info = require('../../services/app_info');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('', auth.checkApiAuthForMigrationPage, wrap(async (req, res, next) => {
|
||||
res.send({
|
||||
db_version: parseInt(await options.getOption('db_version')),
|
||||
app_db_version: app_info.db_version
|
||||
});
|
||||
}));
|
||||
|
||||
router.post('', auth.checkApiAuthForMigrationPage, wrap(async (req, res, next) => {
|
||||
const migrations = await migration.migrate();
|
||||
|
||||
res.send({
|
||||
migrations: migrations
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
31
src/routes/api/note_history.js
Normal file
31
src/routes/api/note_history.js
Normal file
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const auth = require('../../services/auth');
|
||||
const protected_session = require('../../services/protected_session');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
const history = await sql.getAll("SELECT * FROM note_revisions WHERE noteId = ? order by dateModifiedTo desc", [noteId]);
|
||||
protected_session.decryptNoteHistoryRows(req, history);
|
||||
|
||||
res.send(history);
|
||||
}));
|
||||
|
||||
router.put('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await sql.replace("note_revisions", req.body);
|
||||
|
||||
await sync_table.addNoteHistorySync(req.body.noteRevisionId, sourceId);
|
||||
});
|
||||
|
||||
res.send();
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
109
src/routes/api/notes.js
Normal file
109
src/routes/api/notes.js
Normal file
@@ -0,0 +1,109 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../../services/auth');
|
||||
const sql = require('../../services/sql');
|
||||
const notes = require('../../services/notes');
|
||||
const log = require('../../services/log');
|
||||
const utils = require('../../services/utils');
|
||||
const protected_session = require('../../services/protected_session');
|
||||
const tree = require('../../services/tree');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
const detail = await sql.getFirst("SELECT * FROM notes WHERE noteId = ?", [noteId]);
|
||||
|
||||
if (!detail) {
|
||||
log.info("Note " + noteId + " has not been found.");
|
||||
|
||||
return res.status(404).send({});
|
||||
}
|
||||
|
||||
protected_session.decryptNote(req, detail);
|
||||
|
||||
res.send({
|
||||
detail: detail
|
||||
});
|
||||
}));
|
||||
|
||||
router.post('/:parentNoteId/children', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const sourceId = req.headers.source_id;
|
||||
const parentNoteId = req.params.parentNoteId;
|
||||
const newNote = req.body;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
const { noteId, noteTreeId, note } = await notes.createNewNote(parentNoteId, newNote, req, sourceId);
|
||||
|
||||
res.send({
|
||||
'noteId': noteId,
|
||||
'noteTreeId': noteTreeId,
|
||||
'note': note
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const note = req.body;
|
||||
const noteId = req.params.noteId;
|
||||
const sourceId = req.headers.source_id;
|
||||
const dataKey = protected_session.getDataKey(req);
|
||||
|
||||
await notes.updateNote(noteId, note, dataKey, sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const search = '%' + utils.sanitizeSql(req.query.search) + '%';
|
||||
|
||||
// searching in protected notes is pointless because of encryption
|
||||
const noteIds = await sql.getFirstColumn(`SELECT noteId FROM notes
|
||||
WHERE isDeleted = 0 AND isProtected = 0 AND (title LIKE ? OR content LIKE ?)`, [search, search]);
|
||||
|
||||
res.send(noteIds);
|
||||
}));
|
||||
|
||||
router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
const sourceId = req.headers.source_id;
|
||||
const dataKey = protected_session.getDataKey(req);
|
||||
|
||||
await tree.sortNotesAlphabetically(noteId, dataKey, sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put('/:noteId/protect-sub-tree/:isProtected', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
const isProtected = !!parseInt(req.params.isProtected);
|
||||
const dataKey = protected_session.getDataKey(req);
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await notes.protectNoteRecursively(noteId, dataKey, isProtected, sourceId);
|
||||
});
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put(/\/(.*)\/type\/(.*)\/mime\/(.*)/, auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params[0];
|
||||
const type = req.params[1];
|
||||
const mime = req.params[2];
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await sql.execute("UPDATE notes SET type = ?, mime = ?, dateModified = ? WHERE noteId = ?",
|
||||
[type, mime, utils.nowDate(), noteId]);
|
||||
|
||||
await sync_table.addNoteSync(noteId, sourceId);
|
||||
});
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
16
src/routes/api/password.js
Normal file
16
src/routes/api/password.js
Normal file
@@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const changePassword = require('../../services/change_password');
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.post('/change', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const result = await changePassword.changePassword(req.body['current_password'], req.body['new_password'], req);
|
||||
|
||||
res.send(result);
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
25
src/routes/api/recent_changes.js
Normal file
25
src/routes/api/recent_changes.js
Normal file
@@ -0,0 +1,25 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const recentChanges = await sql.getAll(
|
||||
`SELECT
|
||||
notes.isDeleted AS current_isDeleted,
|
||||
notes.title AS current_title,
|
||||
note_revisions.*
|
||||
FROM
|
||||
note_revisions
|
||||
JOIN notes USING(noteId)
|
||||
ORDER BY
|
||||
dateModifiedTo DESC
|
||||
LIMIT 1000`);
|
||||
|
||||
res.send(recentChanges);
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
51
src/routes/api/recent_notes.js
Normal file
51
src/routes/api/recent_notes.js
Normal file
@@ -0,0 +1,51 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const auth = require('../../services/auth');
|
||||
const utils = require('../../services/utils');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const options = require('../../services/options');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
res.send(await getRecentNotes());
|
||||
}));
|
||||
|
||||
router.put('/:noteTreeId/:notePath', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteTreeId = req.params.noteTreeId;
|
||||
const notePath = req.params.notePath;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await sql.replace('recent_notes', {
|
||||
noteTreeId: noteTreeId,
|
||||
notePath: notePath,
|
||||
dateAccessed: utils.nowDate(),
|
||||
isDeleted: 0
|
||||
});
|
||||
|
||||
await sync_table.addRecentNoteSync(noteTreeId, sourceId);
|
||||
|
||||
await options.setOption('start_note_path', notePath, sourceId);
|
||||
});
|
||||
|
||||
res.send(await getRecentNotes());
|
||||
}));
|
||||
|
||||
async function getRecentNotes() {
|
||||
return await sql.getAll(`
|
||||
SELECT
|
||||
recent_notes.*
|
||||
FROM
|
||||
recent_notes
|
||||
JOIN note_tree USING(noteTreeId)
|
||||
WHERE
|
||||
recent_notes.isDeleted = 0
|
||||
AND note_tree.isDeleted = 0
|
||||
ORDER BY
|
||||
dateAccessed DESC`);
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
81
src/routes/api/script.js
Normal file
81
src/routes/api/script.js
Normal file
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
const sql = require('../../services/sql');
|
||||
const notes = require('../../services/notes');
|
||||
const protected_session = require('../../services/protected_session');
|
||||
const attributes = require('../../services/attributes');
|
||||
const script = require('../../services/script');
|
||||
|
||||
router.post('/exec/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
const ret = await script.executeScript(noteId, req, req.body.script, req.body.params);
|
||||
|
||||
res.send(ret);
|
||||
}));
|
||||
|
||||
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
|
||||
|
||||
const scripts = [];
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
scripts.push(await getNoteWithSubtreeScript(noteId, req));
|
||||
}
|
||||
|
||||
res.send(scripts);
|
||||
}));
|
||||
|
||||
router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
const noteScript = (await notes.getNoteById(noteId, req)).content;
|
||||
|
||||
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], req);
|
||||
|
||||
res.send(subTreeScripts + noteScript);
|
||||
}));
|
||||
|
||||
async function getNoteWithSubtreeScript(noteId, req) {
|
||||
const noteScript = (await notes.getNoteById(noteId, req)).content;
|
||||
|
||||
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], req);
|
||||
|
||||
return subTreeScripts + noteScript;
|
||||
}
|
||||
|
||||
async function getSubTreeScripts(parentId, includedNoteIds, dataKey) {
|
||||
const children = await sql.getAll(`SELECT notes.noteId, notes.title, notes.content, notes.isProtected, notes.mime
|
||||
FROM notes JOIN note_tree USING(noteId)
|
||||
WHERE note_tree.isDeleted = 0 AND notes.isDeleted = 0
|
||||
AND note_tree.parentNoteId = ? AND notes.type = 'code'
|
||||
AND (notes.mime = 'application/javascript' OR notes.mime = 'text/html')`, [parentId]);
|
||||
|
||||
protected_session.decryptNotes(dataKey, children);
|
||||
|
||||
let script = "\r\n";
|
||||
|
||||
for (const child of children) {
|
||||
if (includedNoteIds.includes(child.noteId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
includedNoteIds.push(child.noteId);
|
||||
|
||||
script += await getSubTreeScripts(child.noteId, includedNoteIds, dataKey);
|
||||
|
||||
if (child.mime === 'application/javascript') {
|
||||
child.content = '<script>' + child.content + '</script>';
|
||||
}
|
||||
|
||||
script += child.content + "\r\n";
|
||||
}
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
44
src/routes/api/settings.js
Normal file
44
src/routes/api/settings.js
Normal file
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const options = require('../../services/options');
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
// options allowed to be updated directly in settings dialog
|
||||
const ALLOWED_OPTIONS = ['protected_session_timeout', 'history_snapshot_time_interval'];
|
||||
|
||||
router.get('/all', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const settings = await sql.getMap("SELECT name, value FROM options");
|
||||
|
||||
res.send(settings);
|
||||
}));
|
||||
|
||||
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const settings = await sql.getMap("SELECT name, value FROM options WHERE name IN ("
|
||||
+ ALLOWED_OPTIONS.map(x => '?').join(",") + ")", ALLOWED_OPTIONS);
|
||||
|
||||
res.send(settings);
|
||||
}));
|
||||
|
||||
router.post('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const body = req.body;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
if (ALLOWED_OPTIONS.includes(body['name'])) {
|
||||
const optionName = await options.getOption(body['name']);
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await options.setOption(body['name'], body['value'], sourceId);
|
||||
});
|
||||
|
||||
res.send({});
|
||||
}
|
||||
else {
|
||||
res.send("not allowed option to set");
|
||||
}
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
33
src/routes/api/setup.js
Normal file
33
src/routes/api/setup.js
Normal file
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../../services/auth');
|
||||
const options = require('../../services/options');
|
||||
const sql = require('../../services/sql');
|
||||
const utils = require('../../services/utils');
|
||||
const my_scrypt = require('../../services/my_scrypt');
|
||||
const password_encryption = require('../../services/password_encryption');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.post('', auth.checkAppNotInitialized, wrap(async (req, res, next) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await options.setOption('username', username);
|
||||
|
||||
await options.setOption('password_verification_salt', utils.randomSecureToken(32));
|
||||
await options.setOption('password_derived_key_salt', utils.randomSecureToken(32));
|
||||
|
||||
const passwordVerificationKey = utils.toBase64(await my_scrypt.getVerificationHash(password));
|
||||
await options.setOption('password_verification_hash', passwordVerificationKey);
|
||||
|
||||
await password_encryption.setDataKey(password, utils.randomSecureToken(16));
|
||||
});
|
||||
|
||||
sql.setDbReadyAsResolved();
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
26
src/routes/api/sql.js
Normal file
26
src/routes/api/sql.js
Normal file
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../../services/auth');
|
||||
const sql = require('../../services/sql');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.post('/execute', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const query = req.body.query;
|
||||
|
||||
try {
|
||||
res.send({
|
||||
success: true,
|
||||
rows: await sql.getAll(query)
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
res.send({
|
||||
success: false,
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
204
src/routes/api/sync.js
Normal file
204
src/routes/api/sync.js
Normal file
@@ -0,0 +1,204 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../../services/auth');
|
||||
const sync = require('../../services/sync');
|
||||
const syncUpdate = require('../../services/sync_update');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const sql = require('../../services/sql');
|
||||
const options = require('../../services/options');
|
||||
const content_hash = require('../../services/content_hash');
|
||||
const log = require('../../services/log');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('/check', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
res.send({
|
||||
'hashes': await content_hash.getHashes(),
|
||||
'max_sync_id': await sql.getFirstValue('SELECT MAX(id) FROM sync')
|
||||
});
|
||||
}));
|
||||
|
||||
router.post('/now', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
res.send(await sync.sync());
|
||||
}));
|
||||
|
||||
router.post('/fill-sync-rows', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await sql.doInTransaction(async () => {
|
||||
await sync_table.fillAllSyncRows();
|
||||
});
|
||||
|
||||
log.info("Sync rows have been filled.");
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.post('/force-full-sync', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await sql.doInTransaction(async () => {
|
||||
await options.setOption('last_synced_pull', 0);
|
||||
await options.setOption('last_synced_push', 0);
|
||||
});
|
||||
|
||||
log.info("Forcing full sync.");
|
||||
|
||||
// not awaiting for the job to finish (will probably take a long time)
|
||||
sync.sync();
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.post('/force-note-sync/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await sync_table.addNoteSync(noteId);
|
||||
|
||||
for (const noteTreeId of await sql.getFirstColumn("SELECT noteTreeId FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [noteId])) {
|
||||
await sync_table.addNoteTreeSync(noteTreeId);
|
||||
await sync_table.addRecentNoteSync(noteTreeId);
|
||||
}
|
||||
|
||||
for (const noteRevisionId of await sql.getFirstColumn("SELECT noteRevisionId FROM note_revisions WHERE noteId = ?", [noteId])) {
|
||||
await sync_table.addNoteHistorySync(noteRevisionId);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("Forcing note sync for " + noteId);
|
||||
|
||||
// not awaiting for the job to finish (will probably take a long time)
|
||||
sync.sync();
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const lastSyncId = parseInt(req.query.lastSyncId);
|
||||
|
||||
res.send(await sql.getAll("SELECT * FROM sync WHERE id > ?", [lastSyncId]));
|
||||
}));
|
||||
|
||||
router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
res.send({
|
||||
entity: await sql.getFirst("SELECT * FROM notes WHERE noteId = ?", [noteId])
|
||||
});
|
||||
}));
|
||||
|
||||
router.get('/note_tree/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteTreeId = req.params.noteTreeId;
|
||||
|
||||
res.send(await sql.getFirst("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]));
|
||||
}));
|
||||
|
||||
router.get('/note_revisions/:noteRevisionId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteRevisionId = req.params.noteRevisionId;
|
||||
|
||||
res.send(await sql.getFirst("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [noteRevisionId]));
|
||||
}));
|
||||
|
||||
router.get('/options/:name', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const name = req.params.name;
|
||||
const opt = await sql.getFirst("SELECT * FROM options WHERE name = ?", [name]);
|
||||
|
||||
if (!opt.isSynced) {
|
||||
res.send("This option can't be synced.");
|
||||
}
|
||||
else {
|
||||
res.send(opt);
|
||||
}
|
||||
}));
|
||||
|
||||
router.get('/note_reordering/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const parentNoteId = req.params.parentNoteId;
|
||||
|
||||
res.send({
|
||||
parentNoteId: parentNoteId,
|
||||
ordering: await sql.getMap("SELECT noteTreeId, notePosition FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId])
|
||||
});
|
||||
}));
|
||||
|
||||
router.get('/recent_notes/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteTreeId = req.params.noteTreeId;
|
||||
|
||||
res.send(await sql.getFirst("SELECT * FROM recent_notes WHERE noteTreeId = ?", [noteTreeId]));
|
||||
}));
|
||||
|
||||
router.get('/images/:imageId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const imageId = req.params.imageId;
|
||||
const entity = await sql.getFirst("SELECT * FROM images WHERE imageId = ?", [imageId]);
|
||||
|
||||
if (entity && entity.data !== null) {
|
||||
entity.data = entity.data.toString('base64');
|
||||
}
|
||||
|
||||
res.send(entity);
|
||||
}));
|
||||
|
||||
router.get('/note_images/:noteImageId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteImageId = req.params.noteImageId;
|
||||
|
||||
res.send(await sql.getFirst("SELECT * FROM note_images WHERE noteImageId = ?", [noteImageId]));
|
||||
}));
|
||||
|
||||
router.get('/attributes/:attributeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const attributeId = req.params.attributeId;
|
||||
|
||||
res.send(await sql.getFirst("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]));
|
||||
}));
|
||||
|
||||
router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put('/note_tree', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await syncUpdate.updateNoteTree(req.body.entity, req.body.sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put('/note_revisions', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await syncUpdate.updateNoteHistory(req.body.entity, req.body.sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put('/note_reordering', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await syncUpdate.updateNoteReordering(req.body.entity, req.body.sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put('/options', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await syncUpdate.updateOptions(req.body.entity, req.body.sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put('/recent_notes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await syncUpdate.updateRecentNotes(req.body.entity, req.body.sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put('/images', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await syncUpdate.updateImage(req.body.entity, req.body.sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put('/note_images', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await syncUpdate.updateNoteImage(req.body.entity, req.body.sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.put('/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await syncUpdate.updateAttribute(req.body.entity, req.body.sourceId);
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
52
src/routes/api/tree.js
Normal file
52
src/routes/api/tree.js
Normal file
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const options = require('../../services/options');
|
||||
const utils = require('../../services/utils');
|
||||
const auth = require('../../services/auth');
|
||||
const protected_session = require('../../services/protected_session');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const notes = await sql.getAll(`
|
||||
SELECT
|
||||
note_tree.*,
|
||||
notes.title,
|
||||
notes.isProtected,
|
||||
notes.type
|
||||
FROM
|
||||
note_tree
|
||||
JOIN
|
||||
notes ON notes.noteId = note_tree.noteId
|
||||
WHERE
|
||||
notes.isDeleted = 0
|
||||
AND note_tree.isDeleted = 0
|
||||
ORDER BY
|
||||
notePosition`);
|
||||
|
||||
protected_session.decryptNotes(req, notes);
|
||||
|
||||
res.send({
|
||||
notes: notes,
|
||||
start_note_path: await options.getOption('start_note_path')
|
||||
});
|
||||
}));
|
||||
|
||||
router.put('/:noteTreeId/set-prefix', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteTreeId = req.params.noteTreeId;
|
||||
const sourceId = req.headers.source_id;
|
||||
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await sql.execute("UPDATE note_tree SET prefix = ?, dateModified = ? WHERE noteTreeId = ?", [prefix, utils.nowDate(), noteTreeId]);
|
||||
|
||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||
});
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
125
src/routes/api/tree_changes.js
Normal file
125
src/routes/api/tree_changes.js
Normal file
@@ -0,0 +1,125 @@
|
||||
"use strict";
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const auth = require('../../services/auth');
|
||||
const utils = require('../../services/utils');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const tree = require('../../services/tree');
|
||||
const notes = require('../../services/notes');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
/**
|
||||
* Code in this file deals with moving and cloning note tree rows. Relationship between note and parent note is unique
|
||||
* for not deleted note trees. There may be multiple deleted note-parent note relationships.
|
||||
*/
|
||||
|
||||
router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteTreeId = req.params.noteTreeId;
|
||||
const parentNoteId = req.params.parentNoteId;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
const noteToMove = await tree.getNoteTree(noteTreeId);
|
||||
|
||||
if (!await tree.validateParentChild(res, parentNoteId, noteToMove.noteId, noteTreeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(notePosition) FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0', [parentNoteId]);
|
||||
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1;
|
||||
|
||||
const now = utils.nowDate();
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?",
|
||||
[parentNoteId, newNotePos, now, noteTreeId]);
|
||||
|
||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||
});
|
||||
|
||||
res.send({ success: true });
|
||||
}));
|
||||
|
||||
router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteTreeId = req.params.noteTreeId;
|
||||
const beforeNoteTreeId = req.params.beforeNoteTreeId;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
const noteToMove = await tree.getNoteTree(noteTreeId);
|
||||
const beforeNote = await tree.getNoteTree(beforeNoteTreeId);
|
||||
|
||||
if (!await tree.validateParentChild(res, beforeNote.parentNoteId, noteToMove.noteId, noteTreeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
// we don't change dateModified so other changes are prioritized in case of conflict
|
||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||
await sql.execute("UPDATE note_tree SET notePosition = notePosition + 1 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0",
|
||||
[beforeNote.parentNoteId, beforeNote.notePosition]);
|
||||
|
||||
await sync_table.addNoteReorderingSync(beforeNote.parentNoteId, sourceId);
|
||||
|
||||
const now = utils.nowDate();
|
||||
|
||||
await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?",
|
||||
[beforeNote.parentNoteId, beforeNote.notePosition, now, noteTreeId]);
|
||||
|
||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||
});
|
||||
|
||||
res.send({ success: true });
|
||||
}));
|
||||
|
||||
router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteTreeId = req.params.noteTreeId;
|
||||
const afterNoteTreeId = req.params.afterNoteTreeId;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
const noteToMove = await tree.getNoteTree(noteTreeId);
|
||||
const afterNote = await tree.getNoteTree(afterNoteTreeId);
|
||||
|
||||
if (!await tree.validateParentChild(res, afterNote.parentNoteId, noteToMove.noteId, noteTreeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
// we don't change dateModified so other changes are prioritized in case of conflict
|
||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||
await sql.execute("UPDATE note_tree SET notePosition = notePosition + 1 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
|
||||
[afterNote.parentNoteId, afterNote.notePosition]);
|
||||
|
||||
await sync_table.addNoteReorderingSync(afterNote.parentNoteId, sourceId);
|
||||
|
||||
await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?",
|
||||
[afterNote.parentNoteId, afterNote.notePosition + 1, utils.nowDate(), noteTreeId]);
|
||||
|
||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||
});
|
||||
|
||||
res.send({ success: true });
|
||||
}));
|
||||
|
||||
router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const noteTreeId = req.params.noteTreeId;
|
||||
const expanded = req.params.expanded;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await sql.execute("UPDATE note_tree SET isExpanded = ? WHERE noteTreeId = ?", [expanded, noteTreeId]);
|
||||
|
||||
// we don't sync expanded attribute
|
||||
});
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.delete('/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await sql.doInTransaction(async () => {
|
||||
await notes.deleteNote(req.params.noteTreeId, req.headers.source_id);
|
||||
});
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user