diff --git a/routes/api/notes.js b/routes/api/notes.js index b7c8816aa..b6206ed18 100644 --- a/routes/api/notes.js +++ b/routes/api/notes.js @@ -2,11 +2,11 @@ const express = require('express'); const router = express.Router(); +const auth = require('../../services/auth'); const sql = require('../../services/sql'); const options = require('../../services/options'); const utils = require('../../services/utils'); -const audit_category = require('../../services/audit_category'); -const auth = require('../../services/auth'); +const notes = require('../../services/notes'); router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => { let noteId = req.params.noteId; @@ -27,181 +27,38 @@ router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => { }); }); -router.put('/:noteId', async (req, res, next) => { - let noteId = req.params.noteId; - - const detail = await sql.getSingleResult("select * from notes where note_id = ?", [noteId]); - - if (detail.note_clone_id) { - noteId = detail.note_clone_id; - } - +router.post('/:parentNoteId/children', async (req, res, next) => { + let parentNoteId = req.params.parentNoteId; + const browserId = utils.browserId(req); const note = req.body; - const now = utils.nowTimestamp(); + const noteId = await notes.createNewNote(parentNoteId, note, browserId); - const historySnapshotTimeInterval = parseInt(await options.getOption('history_snapshot_time_interval')); - - const historyCutoff = now - historySnapshotTimeInterval; - - let noteHistoryId = await sql.getSingleValue("select note_history_id from notes_history where note_id = ? and date_modified_from >= ?", [noteId, historyCutoff]); - - await sql.doInTransaction(async () => { - if (noteHistoryId) { - await sql.execute("update notes_history set note_title = ?, note_text = ?, encryption = ?, date_modified_to = ? where note_history_id = ?", [ - note.detail.note_title, - note.detail.note_text, - note.detail.encryption, - now, - noteHistoryId - ]); - } - else { - noteHistoryId = utils.randomString(16); - - await sql.execute("insert into notes_history (note_history_id, note_id, note_title, note_text, encryption, date_modified_from, date_modified_to) " + - "values (?, ?, ?, ?, ?, ?, ?)", [ - noteHistoryId, - noteId, - note.detail.note_title, - note.detail.note_text, - note.detail.encryption, - now, - now - ]); - } - - await sql.addNoteHistorySync(noteHistoryId); - - if (note.detail.note_title !== detail.note_title) { - await sql.deleteRecentAudits(audit_category.UPDATE_TITLE, req, noteId); - await sql.addAudit(audit_category.UPDATE_TITLE, req, noteId); - } - - if (note.detail.note_text !== detail.note_text) { - await sql.deleteRecentAudits(audit_category.UPDATE_CONTENT, req, noteId); - await sql.addAudit(audit_category.UPDATE_CONTENT, req, noteId); - } - - if (note.detail.encryption !== detail.encryption) { - await sql.addAudit(audit_category.ENCRYPTION, req, noteId, detail.encryption, note.detail.encryption); - } - - await sql.execute("update notes set note_title = ?, note_text = ?, encryption = ?, date_modified = ? where note_id = ?", [ - note.detail.note_title, - note.detail.note_text, - note.detail.encryption, - now, - noteId]); - - await sql.remove("images", noteId); - - for (const img of note.images) { - img.image_data = atob(img.image_data); - - await sql.insert("images", img); - } - - await sql.remove("links", noteId); - - for (const link in note.links) { - await sql.insert("links", link); - } - - await sql.addNoteSync(noteId); + res.send({ + 'note_id': noteId }); +}); + +router.put('/:noteId', async (req, res, next) => { + const newNote = req.body; + let noteId = req.params.noteId; + const browserId = utils.browserId(req); + + await notes.updateNote(noteId, newNote, browserId); res.send({}); }); router.delete('/:noteId', async (req, res, next) => { + const browserId = utils.browserId(req); + await sql.doInTransaction(async () => { - await deleteNote(req.params.noteId, req); + await notes.deleteNote(req.params.noteId, browserId); }); res.send({}); }); -async function deleteNote(noteId, req) { - const now = utils.nowTimestamp(); - - const children = await sql.getResults("select note_id from notes_tree where note_pid = ? and is_deleted = 0", [noteId]); - - for (const child of children) { - await deleteNote(child.note_id); - } - - await sql.execute("update notes_tree set is_deleted = 1, date_modified = ? where note_id = ?", [now, noteId]); - await sql.execute("update notes set is_deleted = 1, date_modified = ? where note_id = ?", [now, noteId]); - - await sql.addAudit(audit_category.DELETE_NOTE, req, noteId); -} - -router.post('/:parentNoteId/children', async (req, res, next) => { - let parentNoteId = req.params.parentNoteId; - - const note = req.body; - - const noteId = utils.newNoteId(); - - if (parentNoteId === "root") { - parentNoteId = ""; - } - - let newNotePos = 0; - - if (note.target === 'into') { - const res = await sql.getSingleResult('select max(note_pos) as max_note_pos from notes_tree where note_pid = ? and is_deleted = 0', [parentNoteId]); - const maxNotePos = res['max_note_pos']; - - if (maxNotePos === null) // no children yet - newNotePos = 0; - else - newNotePos = maxNotePos + 1 - } - else if (note.target === 'after') { - const afterNote = await sql.getSingleResult('select note_pos from notes_tree where note_id = ?', [note.target_note_id]); - - newNotePos = afterNote.note_pos + 1; - - const now = utils.nowTimestamp(); - - await sql.execute('update notes_tree set note_pos = note_pos + 1, date_modified = ? where note_pid = ? and note_pos > ? and is_deleted = 0', [now, parentNoteId, afterNote['note_pos']]); - } - else { - throw new Error('Unknown target: ' + note.target); - } - - await sql.doInTransaction(async () => { - await sql.addAudit(audit_category.CREATE_NOTE, req, noteId); - - const now = utils.nowTimestamp(); - - await sql.insert("notes", { - 'note_id': noteId, - 'note_title': note.note_title, - 'note_text': '', - 'note_clone_id': '', - 'date_created': now, - 'date_modified': now, - 'encryption': note.encryption - }); - - await sql.insert("notes_tree", { - 'note_id': noteId, - 'note_pid': parentNoteId, - 'note_pos': newNotePos, - 'is_expanded': 0, - 'date_modified': utils.nowTimestamp(), - 'is_deleted': 0 - }); - }); - - res.send({ - 'note_id': noteId - }); -}); - router.get('/', async (req, res, next) => { const search = '%' + req.query.search + '%'; diff --git a/routes/api/notes_move.js b/routes/api/notes_move.js index c921234f8..e9557e60a 100644 --- a/routes/api/notes_move.js +++ b/routes/api/notes_move.js @@ -27,7 +27,7 @@ router.put('/:noteId/moveTo/:parentId', auth.checkApiAuth, async (req, res, next [parentId, newNotePos, now, noteId]); await sql.addNoteTreeSync(noteId); - await sql.addAudit(audit_category.CHANGE_PARENT, req, noteId, null, parentId); + await sql.addAudit(audit_category.CHANGE_PARENT, utils.browserId(req), noteId, null, parentId); }); res.send({}); @@ -50,7 +50,7 @@ router.put('/:noteId/moveBefore/:beforeNoteId', async (req, res, next) => { await sql.addNoteTreeSync(noteId); await sql.addNoteReorderingSync(beforeNote['note_pid']); - await sql.addAudit(audit_category.CHANGE_POSITION, req, beforeNote['note_pid']); + await sql.addAudit(audit_category.CHANGE_POSITION, utils.browserId(req), beforeNote['note_pid']); }); } @@ -74,7 +74,7 @@ router.put('/:noteId/moveAfter/:afterNoteId', async (req, res, next) => { await sql.addNoteTreeSync(noteId); await sql.addNoteReorderingSync(afterNote['note_pid']); - await sql.addAudit(audit_category.CHANGE_POSITION, req, afterNote['note_pid']); + await sql.addAudit(audit_category.CHANGE_POSITION, utils.browserId(req), afterNote['note_pid']); }); } @@ -91,7 +91,7 @@ router.put('/:noteId/expanded/:expanded', async (req, res, next) => { await sql.execute("update notes_tree set is_expanded = ? where note_id = ?", [expanded, noteId]); await sql.addNoteTreeSync(noteId); - await sql.addAudit(audit_category.CHANGE_EXPANDED, req, noteId, null, expanded); + await sql.addAudit(audit_category.CHANGE_EXPANDED, utils.browserId(req), noteId, null, expanded); }); res.send({}); diff --git a/routes/api/settings.js b/routes/api/settings.js index 9596b209f..db08232a3 100644 --- a/routes/api/settings.js +++ b/routes/api/settings.js @@ -6,6 +6,7 @@ const sql = require('../../services/sql'); const options = require('../../services/options'); const audit_category = require('../../services/audit_category'); const auth = require('../../services/auth'); +const utils = require('../../services/utils'); // options allowed to be updated directly in settings dialog const ALLOWED_OPTIONS = ['encryption_session_timeout', 'history_snapshot_time_interval']; @@ -30,7 +31,7 @@ router.post('/', async (req, res, next) => { const optionName = await options.getOption(body['name']); await sql.doInTransaction(async () => { - await sql.addAudit(audit_category.SETTINGS, req, null, optionName, body['value'], body['name']); + await sql.addAudit(audit_category.SETTINGS, utils.browserId(req), null, optionName, body['value'], body['name']); await options.setOption(body['name'], body['value']); }); diff --git a/routes/api/status.js b/routes/api/status.js index 3b27b7f44..9874044b1 100644 --- a/routes/api/status.js +++ b/routes/api/status.js @@ -20,13 +20,13 @@ router.post('', auth.checkApiAuth, async (req, res, next) => { audit_category.UPDATE_TITLE, audit_category.CHANGE_PARENT, audit_category.CHANGE_POSITION]); const currentNoteChangesCount = await sql.getSingleValue("SELECT COUNT(*) FROM audit_log WHERE (browser_id IS NULL OR browser_id != ?) " + - "AND date_modified >= ? AND note_id = ? AND category IN (?)", [browserId, currentNoteLoadTime, currentNoteId, - audit_category.UPDATE_CONTENT]); + "AND date_modified >= ? AND note_id = ? AND category IN (?, ?)", [browserId, currentNoteLoadTime, currentNoteId, + audit_category.UPDATE_TITLE, audit_category.UPDATE_CONTENT]); if (currentNoteChangesCount > 0) { console.log("Current note changed!"); console.log("SELECT COUNT(*) FROM audit_log WHERE (browser_id IS NULL OR browser_id != '" + browserId + "') " + - "AND date_modified >= " + currentNoteLoadTime + " AND note_id = '" + currentNoteId + "' AND category IN ('" + audit_category.UPDATE_CONTENT + "')"); + "AND date_modified >= " + currentNoteLoadTime + " AND note_id = '" + currentNoteId + "' AND category IN ('" + audit_category.UPDATE_TITLE + "', '" + audit_category.UPDATE_CONTENT + "')"); } let changesToPushCount = 0; diff --git a/services/change_password.js b/services/change_password.js index 2aab4f909..7e726c1b3 100644 --- a/services/change_password.js +++ b/services/change_password.js @@ -61,7 +61,7 @@ async function changePassword(currentPassword, newPassword, req = null) { await options.setOption('password_verification_hash', newPasswordVerificationKey); - await sql.addAudit(audit_category.CHANGE_PASSWORD, req); + await sql.addAudit(audit_category.CHANGE_PASSWORD, utils.browserId(req)); }); return { diff --git a/services/notes.js b/services/notes.js new file mode 100644 index 000000000..023de3d01 --- /dev/null +++ b/services/notes.js @@ -0,0 +1,173 @@ +const sql = require('./sql'); +const options = require('./options'); +const utils = require('./utils'); +const notes = require('./notes'); +const audit_category = require('./audit_category'); + +async function createNewNote(parentNoteId, note, browserId) { + const noteId = utils.newNoteId(); + + if (parentNoteId === "root") { + parentNoteId = ""; + } + + let newNotePos = 0; + + if (note.target === 'into') { + const res = await sql.getSingleResult('select max(note_pos) as max_note_pos from notes_tree where note_pid = ? and is_deleted = 0', [parentNoteId]); + const maxNotePos = res['max_note_pos']; + + if (maxNotePos === null) // no children yet + newNotePos = 0; + else + newNotePos = maxNotePos + 1 + } + else if (note.target === 'after') { + const afterNote = await sql.getSingleResult('select note_pos from notes_tree where note_id = ?', [note.target_note_id]); + + newNotePos = afterNote.note_pos + 1; + + const now = utils.nowTimestamp(); + + await sql.execute('update notes_tree set note_pos = note_pos + 1, date_modified = ? where note_pid = ? and note_pos > ? and is_deleted = 0', [now, parentNoteId, afterNote['note_pos']]); + } + else { + throw new Error('Unknown target: ' + note.target); + } + + await sql.doInTransaction(async () => { + await sql.addAudit(audit_category.CREATE_NOTE, browserId, noteId); + + const now = utils.nowTimestamp(); + + await sql.insert("notes", { + 'note_id': noteId, + 'note_title': note.note_title, + 'note_text': '', + 'note_clone_id': '', + 'date_created': now, + 'date_modified': now, + 'encryption': note.encryption + }); + + await sql.insert("notes_tree", { + 'note_id': noteId, + 'note_pid': parentNoteId, + 'note_pos': newNotePos, + 'is_expanded': 0, + 'date_modified': utils.nowTimestamp(), + 'is_deleted': 0 + }); + }); + return noteId; +} + +async function updateNote(noteId, newNote, browserId) { + const origNoteDetail = await sql.getSingleResult("select * from notes where note_id = ?", [noteId]); + + if (origNoteDetail.note_clone_id) { + noteId = origNoteDetail.note_clone_id; + } + + + const now = utils.nowTimestamp(); + + const historySnapshotTimeInterval = parseInt(await options.getOption('history_snapshot_time_interval')); + + const historyCutoff = now - historySnapshotTimeInterval; + + let noteHistoryId = await sql.getSingleValue("select note_history_id from notes_history where note_id = ? and date_modified_from >= ?", [noteId, historyCutoff]); + + await sql.doInTransaction(async () => { + if (noteHistoryId) { + await sql.execute("update notes_history set note_title = ?, note_text = ?, encryption = ?, date_modified_to = ? where note_history_id = ?", [ + newNote.detail.note_title, + newNote.detail.note_text, + newNote.detail.encryption, + now, + noteHistoryId + ]); + } + else { + noteHistoryId = utils.randomString(16); + + await sql.execute("insert into notes_history (note_history_id, note_id, note_title, note_text, encryption, date_modified_from, date_modified_to) " + + "values (?, ?, ?, ?, ?, ?, ?)", [ + noteHistoryId, + noteId, + newNote.detail.note_title, + newNote.detail.note_text, + newNote.detail.encryption, + now, + now + ]); + } + + await sql.addNoteHistorySync(noteHistoryId); + await addNoteAudits(origNoteDetail, newNote.detail, browserId); + + await sql.execute("update notes set note_title = ?, note_text = ?, encryption = ?, date_modified = ? where note_id = ?", [ + newNote.detail.note_title, + newNote.detail.note_text, + newNote.detail.encryption, + now, + noteId]); + + await sql.remove("images", noteId); + + for (const img of newNote.images) { + img.image_data = atob(img.image_data); + + await sql.insert("images", img); + } + + await sql.remove("links", noteId); + + for (const link in newNote.links) { + await sql.insert("links", link); + } + + await sql.addNoteSync(noteId); + }); +} + +async function addNoteAudits(origNote, newNote, browserId) { + const noteId = origNote.note_id; + + if (newNote.note_title !== origNote.note_title) { + await sql.deleteRecentAudits(audit_category.UPDATE_TITLE, browserId, noteId); + await sql.addAudit(audit_category.UPDATE_TITLE, browserId, noteId); + } + + if (newNote.note_text !== origNote.note_text) { + await sql.deleteRecentAudits(audit_category.UPDATE_CONTENT, browserId, noteId); + await sql.addAudit(audit_category.UPDATE_CONTENT, browserId, noteId); + } + + if (newNote.encryption !== origNote.encryption) { + await sql.addAudit(audit_category.ENCRYPTION, browserId, noteId, origNote.encryption, newNote.encryption); + } +} + + +async function deleteNote(noteId, browserId) { + const now = utils.nowTimestamp(); + + const children = await sql.getResults("select note_id from notes_tree where note_pid = ? and is_deleted = 0", [noteId]); + + for (const child of children) { + await deleteNote(child.note_id, browserId); + } + + await sql.execute("update notes_tree set is_deleted = 1, date_modified = ? where note_id = ?", [now, noteId]); + await sql.execute("update notes set is_deleted = 1, date_modified = ? where note_id = ?", [now, noteId]); + + await sql.addAudit(audit_category.DELETE_NOTE, browserId, noteId); +} + +module.exports = { + createNewNote, + updateNote, + addNoteAudits, + deleteNote +} \ No newline at end of file diff --git a/services/sql.js b/services/sql.js index 8f15dfcd3..4c3454cb2 100644 --- a/services/sql.js +++ b/services/sql.js @@ -98,17 +98,7 @@ async function remove(tableName, noteId) { return await execute("DELETE FROM " + tableName + " WHERE note_id = ?", [noteId]); } -async function addAudit(category, req=null, noteId=null, changeFrom=null, changeTo=null, comment=null) { - const browserId = req == null ? null : req.get('x-browser-id'); - - await addAuditWithBrowserId(category, browserId, noteId, changeFrom, changeTo, comment); -} - -async function addSyncAudit(category, sourceId, noteId) { - await addAuditWithBrowserId(category, sourceId, noteId); -} - -async function addAuditWithBrowserId(category, browserId=null, noteId=null, changeFrom=null, changeTo=null, comment=null) { +async function addAudit(category, browserId=null, noteId=null, changeFrom=null, changeTo=null, comment=null) { const now = utils.nowTimestamp(); log.info("audit: " + category + ", browserId=" + browserId + ", noteId=" + noteId + ", from=" + changeFrom + ", to=" + changeTo + ", comment=" + comment); @@ -119,9 +109,7 @@ async function addAuditWithBrowserId(category, browserId=null, noteId=null, chan + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, now, category, browserId, noteId, changeFrom, changeTo, comment]); } -async function deleteRecentAudits(category, req, noteId) { - const browserId = req.get('x-browser-id'); - +async function deleteRecentAudits(category, browserId, noteId) { const deleteCutoff = utils.nowTimestamp() - 10 * 60; await execute("DELETE FROM audit_log WHERE category = ? AND browser_id = ? AND note_id = ? AND date_modified > ?", @@ -206,7 +194,6 @@ module.exports = { execute, executeScript, addAudit, - addSyncAudit, deleteRecentAudits, remove, doInTransaction, diff --git a/services/sync.js b/services/sync.js index f931724d8..314fed997 100644 --- a/services/sync.js +++ b/services/sync.js @@ -10,6 +10,7 @@ const config = require('./config'); const SOURCE_ID = require('./source_id'); const audit_category = require('./audit_category'); const eventLog = require('./event_log'); +const notes = require('./notes'); const SYNC_SERVER = config['Sync']['syncServerHost']; const isSyncSetup = !!SYNC_SERVER; @@ -281,11 +282,7 @@ async function updateNote(entity, links, sourceId) { } await sql.addNoteSync(entity.note_id, sourceId); - - // we don't distinguish between those for now - await sql.addSyncAudit(audit_category.UPDATE_CONTENT, sourceId, entity.note_id); - await sql.addSyncAudit(audit_category.UPDATE_TITLE, sourceId, entity.note_id); - + await notes.addNoteAudits(origNote, entity, sourceId); await eventLog.addNoteEvent(entity.note_id, "Synced note "); }); @@ -305,7 +302,7 @@ async function updateNoteTree(entity, sourceId) { await sql.addNoteTreeSync(entity.note_id, sourceId); - await sql.addSyncAudit(audit_category.UPDATE_TITLE, sourceId, entity.note_id); + await sql.addAudit(audit_category.UPDATE_TITLE, sourceId, entity.note_id); }); logSync("Update/sync note tree " + entity.note_id); @@ -339,7 +336,7 @@ async function updateNoteReordering(entity, sourceId) { }); await sql.addNoteReorderingSync(entity.note_pid, sourceId); - await sql.addSyncAudit(audit_category.CHANGE_POSITION, sourceId, entity.note_pid); + await sql.addAudit(audit_category.CHANGE_POSITION, sourceId, entity.note_pid); }); } diff --git a/services/utils.js b/services/utils.js index f7e78f97e..58a3d951d 100644 --- a/services/utils.js +++ b/services/utils.js @@ -40,6 +40,10 @@ function hmac(secret, value) { return hmac.digest('base64'); } +function browserId(req) { + return req.get('x-browser-id'); +} + module.exports = { randomSecureToken, randomString, @@ -47,5 +51,6 @@ module.exports = { newNoteId, toBase64, fromBase64, - hmac + hmac, + browserId }; \ No newline at end of file