moved all sources to src directory

This commit is contained in:
azivner
2018-01-28 22:18:14 -05:00
parent 669d189ab7
commit 52ad7f64b4
468 changed files with 18 additions and 17 deletions

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

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

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

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

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

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

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

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

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

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

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