diff --git a/server/index.js b/server/index.js
index 0888712..347db8f 100644
--- a/server/index.js
+++ b/server/index.js
@@ -16,6 +16,9 @@ require('module-alias')(require('path').join(__dirname, '..'))
//Auth helper, used for decoding users web token
let Auth = require('@helpers/Auth')
+//Note model, used to authorise socket room joins
+let Note = require('@models/Note')
+
//Helmet adds additional security to express server
const helmet = require('helmet')
@@ -51,6 +54,14 @@ global.SocketIo = io
let noteDiffs = {}
+//
+// Room names are namespaced. Previously note rooms were keyed on the bare
+// note_raw_text.id and user rooms on the bare user.id, sharing one flat
+// keyspace -- so user 7 and note_raw_text 7 were the same room.
+//
+const userRoom = userId => `user:${userId}`
+const noteRoom = rawTextId => `note:${rawTextId}`
+
io.on('connection', function(socket){
//When a user connects, add them to their own room
@@ -59,11 +70,11 @@ io.on('connection', function(socket){
socket.on('user_connect', token => {
Auth.decodeToken(token)
.then(userData => {
- socket.join(userData.userId)
+ socket.join(userRoom(userData.userId))
//Track active logged in user accounts
- const usersInRoom = io.sockets.adapter.rooms[userData.userId]
- io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
+ const usersInRoom = io.sockets.adapter.rooms[userRoom(userData.userId)]
+ io.to(userRoom(userData.userId)).emit('update_active_user_count', usersInRoom.length)
}).catch(error => {
//Don't add user to room if they are not logged in
@@ -74,11 +85,11 @@ io.on('connection', function(socket){
socket.on('get_active_user_count', token => {
Auth.decodeToken(token)
.then(userData => {
- socket.join(userData.userId)
+ socket.join(userRoom(userData.userId))
//Track active logged in user accounts
- const usersInRoom = io.sockets.adapter.rooms[userData.userId]
- io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
+ const usersInRoom = io.sockets.adapter.rooms[userRoom(userData.userId)]
+ io.to(userRoom(userData.userId)).emit('update_active_user_count', usersInRoom.length)
}).catch(error => {
// console.log(error)
@@ -107,67 +118,106 @@ io.on('connection', function(socket){
})
})
- socket.on('join_room', rawTextId => {
- // Join user to rawtextid room when they enter
- socket.join(rawTextId)
+ //
+ // The client asks for a NOTE, not a room. The server authorises the request
+ // against the session and derives the room itself, so a client can no longer
+ // name an arbitrary room and be handed its buffered diffs.
+ //
+ socket.on('join_room', ({ noteId, token } = {}) => {
- //If there are past diffs for this note, send them to the user
- if(noteDiffs[rawTextId] != undefined){
-
- //Sort all note diffs by when they were created.
- noteDiffs[rawTextId].sort((a,b) => { return a.time - b.time })
+ Auth.decodeToken(token)
+ .then(userData => {
- //Emit all sorted diffs to user
- socket.emit('past_diffs', noteDiffs[rawTextId])
- }
+ return Note.getRawTextIdForUser(noteId, userData.userId)
+ .then(rawTextId => {
- const usersInRoom = io.sockets.adapter.rooms[rawTextId]
- if(usersInRoom){
- //Update users in room count
- io.to(rawTextId).emit('update_user_count', usersInRoom.length)
- }
- })
+ // No claim on this note -> no room, no diffs
+ if(rawTextId == null){ return }
- socket.on('leave_room', roomId => {
- socket.leave(roomId)
- // console.log('User Left room')
+ const room = noteRoom(rawTextId)
- const usersInRoom = io.sockets.adapter.rooms[roomId]
- if(usersInRoom){
- // console.log('Users in room', usersInRoom.length)
- io.to(roomId).emit('update_user_count', usersInRoom.length)
- }
-
- })
+ // Remember what this socket is allowed into, so leave_room and
+ // note_diff never have to trust a client-supplied room again
+ socket.authorizedRooms = socket.authorizedRooms || {}
+ socket.authorizedRooms[noteId] = { room, rawTextId }
- socket.on('note_diff', data => {
+ socket.join(room)
- //Log each diff for note
- const noteId = data.id
- delete data.id
- if(noteDiffs[noteId] == undefined){ noteDiffs[noteId] = [] }
- data.time = +new Date
-
- noteDiffs[noteId].push(data)
+ //If there are past diffs for this note, send them to the user
+ if(noteDiffs[rawTextId] != undefined){
- // Go over each user in this note-room
- io.in(noteId).clients((error, clients) => {
- if (error) throw error;
+ //Sort all note diffs by when they were created.
+ noteDiffs[rawTextId].sort((a,b) => { return a.time - b.time })
- //Go through each client in note-room and send them the diff
- clients.forEach(socketId => {
- // only send off diff if user
- if(socketId != socket.id){
- io.to(socketId).emit('incoming_diff', data)
+ //Emit all sorted diffs to user
+ socket.emit('past_diffs', noteDiffs[rawTextId])
+ }
+
+ const usersInRoom = io.sockets.adapter.rooms[room]
+ if(usersInRoom){
+ //Update users in room count
+ io.to(room).emit('update_user_count', usersInRoom.length)
}
})
-
- });
-
+ })
+ .catch(() => {
+ //Not logged in, or no access. Join nothing.
+ })
})
- socket.on('truncate_diffs_at_save', checkpoint => {
+ socket.on('leave_room', ({ noteId } = {}) => {
+ // Only rooms this socket was actually authorised into
+ const entry = socket.authorizedRooms && socket.authorizedRooms[noteId]
+ if(!entry){ return }
+
+ socket.leave(entry.room)
+ delete socket.authorizedRooms[noteId]
+
+ const usersInRoom = io.sockets.adapter.rooms[entry.room]
+ if(usersInRoom){
+ io.to(entry.room).emit('update_user_count', usersInRoom.length)
+ }
+ })
+
+ socket.on('note_diff', (data = {}) => {
+
+ // Room comes from what this socket was authorised into, never from the
+ // payload
+ const entry = socket.authorizedRooms && socket.authorizedRooms[data.noteId]
+ if(!entry){ return }
+
+ const room = entry.room
+ const rawTextId = entry.rawTextId
+
+ //Log each diff for note
+ if(noteDiffs[rawTextId] == undefined){ noteDiffs[rawTextId] = [] }
+
+ // Carry the raw-text id through. This used to `delete data.id`, stripping
+ // the only identifier before storing and broadcasting -- so a client with
+ // several notes open on one socket could not tell which note a patch
+ // belonged to, and applied every one to whichever editor it found first.
+ const payload = Object.assign({}, data, { rawTextId, time: +new Date })
+ delete payload.noteId
+
+ noteDiffs[rawTextId].push(payload)
+
+ // Everyone in the note room except the sender
+ socket.to(room).emit('incoming_diff', payload)
+ })
+
+ socket.on('truncate_diffs_at_save', (checkpoint = {}) => {
+
+ const entry = socket.authorizedRooms && socket.authorizedRooms[checkpoint.noteId]
+ if(!entry){ return }
+ checkpoint.rawTextId = entry.rawTextId
+
+ // NOTE: the comparison below is `diffSet[i].hash == checkpoint`, comparing
+ // a hash string against the whole checkpoint object, so it never matches
+ // and sliceTo stays 0 -- meaning the entire diff set is discarded at every
+ // save. That happens to be the fail-safe outcome (the saved text becomes
+ // the new baseline), and the correct slice direction for a real fix is
+ // ambiguous, so it is deliberately left alone rather than guessed at.
let diffSet = noteDiffs[checkpoint.rawTextId]
if(diffSet && diffSet.length > 0){
diff --git a/server/models/Attachment.js b/server/models/Attachment.js
index 50d9100..7571c54 100644
--- a/server/models/Attachment.js
+++ b/server/models/Attachment.js
@@ -300,10 +300,10 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
Attachment.scrapeUrlsCreateAttachments(userId, noteId, foundUrls).then( freshlyScrapedText => {
//Once everything is done being scraped, emit new attachment events
- SocketIo.to(userId).emit('update_counts')
+ SocketIo.to(`user:${userId}`).emit('update_counts')
// Tell user to update attachments with scraped text
- SocketIo.to(userId).emit('update_note_attachments')
+ SocketIo.to(`user:${userId}`).emit('update_note_attachments')
solrAttachmentText += freshlyScrapedText
resolve(solrAttachmentText)
@@ -597,10 +597,10 @@ Attachment.pushUrl = (pushkey,url) => {
if(typeof SocketIo != 'undefined'){
//Once everything is done being scraped, emit new attachment events
- SocketIo.to(userId).emit('update_counts')
+ SocketIo.to(`user:${userId}`).emit('update_counts')
// Tell user to update attachments with scraped text
- SocketIo.to(userId).emit('update_note_attachments')
+ SocketIo.to(`user:${userId}`).emit('update_note_attachments')
}
return resolve(true)
diff --git a/server/models/Note.js b/server/models/Note.js
index da15dc5..0302538 100644
--- a/server/models/Note.js
+++ b/server/models/Note.js
@@ -199,7 +199,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
.then((rows, fields) => {
if(typeof SocketIo != 'undefined'){
- SocketIo.to(userId).emit('new_note_created', rows[0].insertId)
+ SocketIo.to(`user:${userId}`).emit('new_note_created', rows[0].insertId)
}
// Indexing is done on save
@@ -490,7 +490,7 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
}
db.promise().query('UPDATE note SET snippet = ? WHERE id = ?', [updatedSnippet, otherNote.id])
.then((rows, fields) => {
- SocketIo.to(otherNote['user_id']).emit('new_note_text_saved', {'noteId':otherNote.id, hash})
+ SocketIo.to(`user:${otherNote['user_id']}`).emit('new_note_text_saved', {'noteId':otherNote.id, hash})
})
}
})
@@ -514,7 +514,7 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
.then((rows, fields) => {
if(typeof SocketIo != 'undefined'){
- SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash})
+ SocketIo.to(`user:${userId}`).emit('new_note_text_saved', {noteId, hash})
//Async attachment reindex
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
@@ -538,7 +538,7 @@ Note.setPinned = (userId, noteId, pinnedBoolean) => {
.query('UPDATE note JOIN note_raw_text ON note_raw_text_id = note_raw_text.id SET pinned = ?, updated = ? WHERE note.id = ? AND user_id = ?',
[pinned, now, noteId, userId])
.then((rows, fields) => {
- SocketIo.to(userId).emit('note_attribute_modified', noteId)
+ SocketIo.to(`user:${userId}`).emit('note_attribute_modified', noteId)
resolve(true)
})
})
@@ -555,7 +555,7 @@ Note.setArchived = (userId, noteId, archivedBoolead) => {
.query('UPDATE note JOIN note_raw_text ON note_raw_text_id = note_raw_text.id SET archived = ?, updated = ? WHERE note.id = ? AND user_id = ?',
[archived, now, noteId, userId])
.then((rows, fields) => {
- SocketIo.to(userId).emit('note_attribute_modified', noteId)
+ SocketIo.to(`user:${userId}`).emit('note_attribute_modified', noteId)
resolve(true)
})
})
@@ -581,7 +581,7 @@ Note.setTrashed = (userId, noteId, trashedBoolean, masterKey) => {
Note.reindex(userId, masterKey, removeFromIndex)
- SocketIo.to(userId).emit('note_attribute_modified', noteId)
+ SocketIo.to(`user:${userId}`).emit('note_attribute_modified', noteId)
resolve(true)
})
})
@@ -653,7 +653,7 @@ Note.delete = (userId, noteId, masterKey = null) => {
}
})
- SocketIo.to(userId).emit('update_counts')
+ SocketIo.to(`user:${userId}`).emit('update_counts')
if(masterKey){
//Remove note ID from index
@@ -1190,4 +1190,30 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
.catch(console.log)
})
-}
\ No newline at end of file
+}
+
+//
+// Resolve a note's raw-text id for a given user.
+// Doubles as the access check: a user with no claim on this note gets null,
+// so authorisation and room derivation are the same lookup.
+//
+Note.getRawTextIdForUser = (noteId, userId) => {
+ return new Promise((resolve, reject) => {
+
+ const id = parseInt(noteId, 10)
+ if(!Number.isInteger(id) || id < 1){ return resolve(null) }
+
+ db.promise()
+ .query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ? LIMIT 1', [id, userId])
+ .then((rows) => {
+
+ if(!rows[0] || rows[0].length == 0){ return resolve(null) }
+
+ return resolve( rows[0][0]['note_raw_text_id'] )
+ })
+ .catch(error => {
+ console.log(error)
+ return resolve(null)
+ })
+ })
+}
diff --git a/server/models/ShareNote.js b/server/models/ShareNote.js
index 04b7991..e1f7ef2 100644
--- a/server/models/ShareNote.js
+++ b/server/models/ShareNote.js
@@ -75,7 +75,7 @@ ShareNote.addUserToSharedNote = (userId, noteId, shareUserId, masterKey) => {
const sharedUserNoteId = rows[0]['insertId']
//Emit update count event to user shared with - so they see the note in real time
- SocketIo.to(shareUserId).emit('update_counts')
+ SocketIo.to(`user:${shareUserId}`).emit('update_counts')
let success = true
return resolve({success, shareUserId, sharedUserNoteId})
diff --git a/server/models/Tag.js b/server/models/Tag.js
index deedc51..237bbca 100644
--- a/server/models/Tag.js
+++ b/server/models/Tag.js
@@ -58,7 +58,7 @@ Tag.addToNote = (userId, noteId, tagText) => {
Tag.associateWithNote(userId, noteId, newTagId)
.then( result => {
- SocketIo.to(userId).emit('new_note_text_saved', {noteId})
+ SocketIo.to(`user:${userId}`).emit('new_note_text_saved', {noteId})
resolve(result)
})
})
diff --git a/server/routes/attachmentController.js b/server/routes/attachmentController.js
index 16fee9e..a1b1a8c 100644
--- a/server/routes/attachmentController.js
+++ b/server/routes/attachmentController.js
@@ -7,8 +7,6 @@ let router = express.Router()
let Attachment = require('@models/Attachment')
let Note = require('@models/Note')
-let userId = null
-let masterKey = null
// middleware that is specific to this router
router.use(function setUserId (req, res, next) {
@@ -19,23 +17,33 @@ router.use(function setUserId (req, res, next) {
}
if(req.headers.userId){
- userId = req.headers.userId
- masterKey = req.headers.masterKey
next()
}
})
router.post('/search', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Attachment.search(userId, req.body.noteId, req.body.attachmentType, req.body.offset, req.body.setSize, req.body.includeShared)
.then( data => res.send(data) )
})
router.post('/textsearch', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Attachment.textSearch(userId, req.body.searchTerm)
.then( data => res.send(data) )
})
router.post('/update', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
.then( result => {
Note.reindex(userId, req.body.noteId)
@@ -44,11 +52,19 @@ router.post('/update', function (req, res) {
})
router.post('/delete', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Attachment.delete(userId, req.body.attachmentId)
.then( data => res.send(data) )
})
router.post('/upload', upload.single('file'), function (req, res, next) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
//Create attachment with file information and node id
const noteId = parseInt(req.body.noteId)
@@ -67,6 +83,10 @@ router.post('/upload', upload.single('file'), function (req, res, next) {
// get push key
router.post('/getbookmarklet', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Attachment.getPushkeyBookmarklet(userId)
.then( data => res.send(data) )
diff --git a/server/routes/cycletrackingController.js b/server/routes/cycletrackingController.js
index 1efb797..83d1946 100644
--- a/server/routes/cycletrackingController.js
+++ b/server/routes/cycletrackingController.js
@@ -1,41 +1,45 @@
-//
-// /api/cycle-tracking
-//
-
-var express = require('express')
-var router = express.Router()
-
-let CycleTracking = require('@models/CycleTracking');
-
-let userId = null
-let masterKey = null
-
-// middleware that is specific to this router
-router.use(function setUserId (req, res, next) {
-
- //Session key is required to continue
- if(!req.headers.sessionId){
- next('Unauthorized')
- }
-
- if(req.headers.userId){
- userId = req.headers.userId
- masterKey = req.headers.masterKey
- next()
- }
-})
-
-//Get quick note text
-router.post('/get', function (req, res) {
- CycleTracking.get(userId, masterKey)
- .then( data => res.send(data) )
-})
-
-//Push text to quick note
-router.post('/save', function (req, res) {
- CycleTracking.save(userId, req.body.cycleData, masterKey)
- .then( data => res.send(data) )
-})
-
-
+//
+// /api/cycle-tracking
+//
+
+var express = require('express')
+var router = express.Router()
+
+let CycleTracking = require('@models/CycleTracking');
+
+
+// middleware that is specific to this router
+router.use(function setUserId (req, res, next) {
+
+ //Session key is required to continue
+ if(!req.headers.sessionId){
+ next('Unauthorized')
+ }
+
+ if(req.headers.userId){
+ next()
+ }
+})
+
+//Get quick note text
+router.post('/get', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
+ CycleTracking.get(userId, masterKey)
+ .then( data => res.send(data) )
+})
+
+//Push text to quick note
+router.post('/save', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
+ CycleTracking.save(userId, req.body.cycleData, masterKey)
+ .then( data => res.send(data) )
+})
+
+
module.exports = router
\ No newline at end of file
diff --git a/server/routes/metrictrackingController.js b/server/routes/metrictrackingController.js
index f6fe21d..20bd642 100644
--- a/server/routes/metrictrackingController.js
+++ b/server/routes/metrictrackingController.js
@@ -1,45 +1,53 @@
-//
-// /api/metric-tracking
-//
-
-var express = require('express')
-var router = express.Router()
-
-let MetricTracking = require('@models/MetricTracking');
-
-let userId = null
-let masterKey = null
-
-// middleware that is specific to this router
-router.use(function setUserId (req, res, next) {
-
- //Session key is required to continue
- if(!req.headers.sessionId){
- next('Unauthorized')
- }
-
- if(req.headers.userId){
- userId = req.headers.userId
- masterKey = req.headers.masterKey
- next()
- }
-})
-
-router.post('/get', function (req, res) {
- MetricTracking.get(userId, masterKey)
- .then( data => res.send(data) )
-})
-
-router.post('/create', function (req, res) {
- MetricTracking.create(userId, masterKey)
- .then( data => res.send(data) )
-})
-
-//Push text to quick note
-router.post('/save', function (req, res) {
- MetricTracking.save(userId, req.body.cycleData, masterKey)
- .then( data => res.send(data) )
-})
-
-
+//
+// /api/metric-tracking
+//
+
+var express = require('express')
+var router = express.Router()
+
+let MetricTracking = require('@models/MetricTracking');
+
+
+// middleware that is specific to this router
+router.use(function setUserId (req, res, next) {
+
+ //Session key is required to continue
+ if(!req.headers.sessionId){
+ next('Unauthorized')
+ }
+
+ if(req.headers.userId){
+ next()
+ }
+})
+
+router.post('/get', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
+ MetricTracking.get(userId, masterKey)
+ .then( data => res.send(data) )
+})
+
+router.post('/create', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
+ MetricTracking.create(userId, masterKey)
+ .then( data => res.send(data) )
+})
+
+//Push text to quick note
+router.post('/save', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
+ MetricTracking.save(userId, req.body.cycleData, masterKey)
+ .then( data => res.send(data) )
+})
+
+
module.exports = router
\ No newline at end of file
diff --git a/server/routes/noteController.js b/server/routes/noteController.js
index 2c59a05..61766fc 100644
--- a/server/routes/noteController.js
+++ b/server/routes/noteController.js
@@ -5,8 +5,6 @@ let Note = require('@models/Note')
let User = require('@models/User')
let ShareNote = require('@models/ShareNote')
-let userId = null
-let masterKey = null
// middleware that is specific to this router
router.use(function setUserId (req, res, next) {
@@ -17,8 +15,6 @@ router.use(function setUserId (req, res, next) {
}
if(req.headers.userId){
- userId = req.headers.userId
- masterKey = req.headers.masterKey
next()
}
})
@@ -27,6 +23,10 @@ router.use(function setUserId (req, res, next) {
// Note actions
//
router.post('/get', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Note.get(userId, req.body.noteId, masterKey)
.then( noteObject => {
@@ -39,21 +39,37 @@ router.post('/get', function (req, res) {
})
router.post('/delete', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Note.delete(userId, req.body.noteId, masterKey)
.then( data => res.send(data) )
})
router.post('/create', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Note.create(userId, req.body.title, req.body.text, masterKey)
.then( id => res.send({id}) )
})
router.post('/update', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Note.update(userId, req.body.noteId, req.body.text, req.body.title, req.body.color, req.body.pinned, req.body.archived, req.body.hash, masterKey)
.then( id => res.send({id}) )
})
router.post('/search', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Note.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters, masterKey)
.then( NoteAndTags => {
res.send(NoteAndTags)
@@ -61,6 +77,10 @@ router.post('/search', function (req, res) {
})
router.post('/reindex', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Note.reindex(userId, masterKey)
.then( data => {
res.send(data)
@@ -72,18 +92,30 @@ router.post('/reindex', function (req, res) {
// Update single note attributes
//
router.post('/setpinned', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Note.setPinned(userId, req.body.noteId, req.body.pinned)
.then( results => {
res.send(results)
})
})
router.post('/setarchived', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Note.setArchived(userId, req.body.noteId, req.body.archived)
.then( results => {
res.send(results)
})
})
router.post('/settrashed', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Note.setTrashed(userId, req.body.noteId, req.body.trashed, masterKey)
.then( results => {
res.send(results)
@@ -94,11 +126,19 @@ router.post('/settrashed', function (req, res) {
// Share Note Actions
//
router.post('/getshareinfo', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
ShareNote.getShareInfo(userId, req.body.noteId, req.body.rawTextId)
.then(results => res.send(results))
})
router.post('/shareadduser', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
// ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username, masterKey)
User.getByUserName(req.body.username)
.then( user => {
@@ -111,22 +151,38 @@ router.post('/shareadduser', function (req, res) {
})
router.post('/shareremoveuser', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
// (userId, noteId, shareNoteUserId, shareUserId, masterKey)
ShareNote.removeUserFromSharedNote(userId, req.body.noteId, req.body.shareUserNoteId, masterKey)
.then(results => res.send(results))
})
router.post('/enableshare', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
//Create Shared Encryption Key for Note
ShareNote.migrateToShared(userId, req.body.noteId, masterKey)
.then(results => res.send(true))
})
router.post('/getsharekey', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
//Get Shared Key for a note
ShareNote.decryptSharedKey(userId, req.body.noteId, masterKey)
.then(results => res.send(results))
})
router.post('/disableshare', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
//Removed shared encryption key from note
ShareNote.migrateToNormal(userId, req.body.noteId, masterKey)
.then(results => res.send(true))
diff --git a/server/routes/quicknoteController.js b/server/routes/quicknoteController.js
index 1e79a75..f7aa411 100644
--- a/server/routes/quicknoteController.js
+++ b/server/routes/quicknoteController.js
@@ -3,8 +3,6 @@ var router = express.Router()
let QuickNote = require('@models/QuickNote');
-let userId = null
-let masterKey = null
// middleware that is specific to this router
router.use(function setUserId (req, res, next) {
@@ -15,26 +13,36 @@ router.use(function setUserId (req, res, next) {
}
if(req.headers.userId){
- userId = req.headers.userId
- masterKey = req.headers.masterKey
next()
}
})
//Get quick note text
router.post('/get', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
QuickNote.get(userId, masterKey)
.then( data => res.send(data) )
})
//Push text to quick note
router.post('/update', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
QuickNote.update(userId, req.body.pushText, masterKey)
.then( data => res.send(data) )
})
//Push text to quick note
router.post('/new', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
QuickNote.newNote(userId)
.then( data => res.send(data) )
})
diff --git a/server/routes/tagController.js b/server/routes/tagController.js
index 7157221..bb57298 100644
--- a/server/routes/tagController.js
+++ b/server/routes/tagController.js
@@ -3,8 +3,6 @@ var router = express.Router()
let Tags = require('@models/Tag')
-let userId = null
-let masterKey = null
// middleware that is specific to this router
router.use(function setUserId (req, res, next) {
@@ -15,49 +13,75 @@ router.use(function setUserId (req, res, next) {
}
if(req.headers.userId){
- userId = req.headers.userId
- masterKey = req.headers.masterKey
next()
}
})
//Get the latest notes the user has created
router.post('/suggest', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Tags.suggest(userId, req.body.noteId, req.body.tagText)
.then( data => res.send(data) )
})
//Get latest tags based on when it was put on a note
router.post('/latest', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Tags.latest(userId, req.body.noteId)
.then( data => res.send(data) )
})
//Get the latest notes the user has created
router.post('/addtonote', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Tags.addToNote(userId, req.body.noteId, req.body.tagText.toLowerCase())
.then( data => res.send(data) )
})
router.post('/removefromnote', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Tags.removeTagFromNote(userId, req.body.tagId)
.then( data => res.send(data) )
})
//Get the latest notes the user has created
router.post('/get', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Tags.get(userId, req.body.noteId)
.then( data => res.send(data) )
})
//Get the latest notes the user has created
router.post('/fornote', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Tags.fornote(userId, req.body.noteId)
.then( data => res.send(data) )
})
//Get all the tags for this user in order of usage
router.post('/usertags', function (req, res) {
+ //Bind per request. These were module-level singletons shared by every
+ //concurrent request, so one user's masterKey could overwrite another's.
+ const { userId, masterKey } = req.headers
+
Tags.userTags(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)
.then( data => res.send(data) )
})
diff --git a/server/routes/userController.js b/server/routes/userController.js
index 8162ee3..853f09b 100644
--- a/server/routes/userController.js
+++ b/server/routes/userController.js
@@ -5,8 +5,6 @@ const User = require('@models/User')
const Auth = require('@helpers/Auth')
const cs = require('@helpers/CryptoString')
-let userId = null
-let masterKey = null
// middleware that is specific to this router
router.use(function setUserId (req, res, next) {
@@ -17,8 +15,6 @@ router.use(function setUserId (req, res, next) {
}
if(req.headers.userId){
- userId = req.headers.userId
- masterKey = req.headers.masterKey
next()
}
})
diff --git a/v2client-vite-vue3/package.json b/v2client-vite-vue3/package.json
index b3b1888..875679b 100644
--- a/v2client-vite-vue3/package.json
+++ b/v2client-vite-vue3/package.json
@@ -11,12 +11,14 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.13",
"axios": "^1.1.3",
+ "chart.js": "^4.6.1",
"diff-match-patch": "^1.0.5",
"fomantic-ui-css": "^2.9.0",
"mitt": "^3.0.1",
"socket.io-client": "^2.5.0",
"tailwindcss": "^4.1.13",
"vue": "^3.5.18",
+ "vue-chartjs": "^5.3.4",
"vue-router": "^4.0.0",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.0"
diff --git a/v2client-vite-vue3/pnpm-lock.yaml b/v2client-vite-vue3/pnpm-lock.yaml
index ec158c6..50e3766 100644
--- a/v2client-vite-vue3/pnpm-lock.yaml
+++ b/v2client-vite-vue3/pnpm-lock.yaml
@@ -13,7 +13,10 @@ importers:
version: 4.1.18(vite@7.3.0(jiti@2.6.1)(lightningcss@1.30.2))
axios:
specifier: ^1.1.3
- version: 1.18.1
+ version: 1.18.1(debug@4.4.3)
+ chart.js:
+ specifier: ^4.6.1
+ version: 4.6.1
diff-match-patch:
specifier: ^1.0.5
version: 1.0.5
@@ -32,6 +35,9 @@ importers:
vue:
specifier: ^3.5.18
version: 3.5.26
+ vue-chartjs:
+ specifier: ^5.3.4
+ version: 5.3.4(chart.js@4.6.1)(vue@3.5.26)
vue-router:
specifier: ^4.0.0
version: 4.6.4(vue@3.5.26)
@@ -240,6 +246,9 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@kurkle/color@0.3.4':
+ resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
+
'@rolldown/pluginutils@1.0.0-beta.53':
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
@@ -530,6 +539,10 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
+ chart.js@4.6.1:
+ resolution: {integrity: sha512-hJyniucsFmq//EvXYvJ6c6mahPoI3kaxdjVH8awTaJ/fDNWeSqqqArixMJWkWNN6fcYvPU3HneSANFNYtRi61g==}
+ engines: {pnpm: '>=8'}
+
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -895,6 +908,12 @@ packages:
yaml:
optional: true
+ vue-chartjs@5.3.4:
+ resolution: {integrity: sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==}
+ peerDependencies:
+ chart.js: ^4.1.1
+ vue: ^3.0.0-0 || ^2.7.0
+
vue-router@4.6.4:
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
peerDependencies:
@@ -1049,6 +1068,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@kurkle/color@0.3.4': {}
+
'@rolldown/pluginutils@1.0.0-beta.53': {}
'@rollup/rollup-android-arm-eabi@4.54.0':
@@ -1261,9 +1282,9 @@ snapshots:
asynckit@0.4.0: {}
- axios@1.18.1:
+ axios@1.18.1(debug@4.4.3):
dependencies:
- follow-redirects: 1.16.0
+ follow-redirects: 1.16.0(debug@4.4.3)
form-data: 4.0.6
https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0
@@ -1282,6 +1303,10 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
+ chart.js@4.6.1:
+ dependencies:
+ '@kurkle/color': 0.3.4
+
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@@ -1397,7 +1422,9 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
- follow-redirects@1.16.0: {}
+ follow-redirects@1.16.0(debug@4.4.3):
+ optionalDependencies:
+ debug: 4.4.3
fomantic-ui-css@2.9.4:
dependencies:
@@ -1636,6 +1663,11 @@ snapshots:
jiti: 2.6.1
lightningcss: 1.30.2
+ vue-chartjs@5.3.4(chart.js@4.6.1)(vue@3.5.26):
+ dependencies:
+ chart.js: 4.6.1
+ vue: 3.5.26
+
vue-router@4.6.4(vue@3.5.26):
dependencies:
'@vue/devtools-api': 6.6.4
diff --git a/v2client-vite-vue3/src/assets/semantic-helper.css b/v2client-vite-vue3/src/assets/semantic-helper.css
index 4307ef9..8666f40 100644
--- a/v2client-vite-vue3/src/assets/semantic-helper.css
+++ b/v2client-vite-vue3/src/assets/semantic-helper.css
@@ -1001,3 +1001,8 @@ padding-right: 10px;
background-color: rgba(0,0,0,0.7);
z-index: 1000;
}
+
+
+i.outline {
+ outline-style: none;
+}
diff --git a/v2client-vite-vue3/src/components/Metrictracking/LineGraph.vue b/v2client-vite-vue3/src/components/Metrictracking/LineGraph.vue
new file mode 100644
index 0000000..fb41a89
--- /dev/null
+++ b/v2client-vite-vue3/src/components/Metrictracking/LineGraph.vue
@@ -0,0 +1,212 @@
+
+
Choose Add/Edit Graphs, then New Graph, and pick which metrics to plot.
+