From 6bb856689d9353e65afd31159d62a3339c1a09d4 Mon Sep 17 00:00:00 2001 From: Max G Date: Sun, 7 Jun 2020 20:57:35 +0000 Subject: [PATCH] * Adjusted theme colors to add more contrast on white theme while making black more OLED friendly * Links now get an underline on hover * Cleaned up CSS variable names, added another theme color for more control * Cleaned up unused CSS, removed scrollbars popping up, tons of other little UI tweaks * Renamed shared notes to inbox * Tweaked form display, seperated login and create accouts * Put login/sign up form on home page * Created more legitimate marketing for home page * Tons up updates to note page and note input panel * Better support for two users editing a note * MUCH better diff handling, web sockets restore notes with unsaved diffs * Moved all squire text modifier functions into a mixin class * It now says saving when closing a note * Lots of cleanup and better handiling of events on mount and destroy * Scroll behavior modified to load notes when closer to bottom of page * Pretty decent shared notes and sharable link support * Updated help text * Search now includes tag suggestions and attachment suggestions * Cleaned up scratch pad a ton, allow for users to create new scratch pads * Created a 404 Page and a Shared note page * So many other small improvements. Oh my god, what is wrong with me, not doing commits!? --- client/src/Helpers.js | 5 + client/src/assets/semantic-helper.css | 85 +- .../src/components/AttachmentDisplayCard.vue | 1 + client/src/components/FastFilters.vue | 4 +- client/src/components/GlobalSiteMenu.vue | 46 +- .../src/components/LoadingIconComponent.vue | 20 +- client/src/components/LoginFormComponent.vue | 139 ++-- client/src/components/NoteInputPanel.vue | 733 ++++++++---------- client/src/components/NoteTagEdit.vue | 2 +- .../src/components/NoteTitleDisplayCard.vue | 41 +- client/src/components/SearchInput.vue | 38 +- client/src/components/ShareNoteComponent.vue | 62 +- .../src/components/SideSlideMenuComponent.vue | 4 +- client/src/mixins/SquireButtonFunctions.js | 344 ++++++++ client/src/pages/HelpPage.vue | 2 +- client/src/pages/HomePage.vue | 53 +- client/src/pages/LoginPage.vue | 45 +- client/src/pages/NotFoundPage.vue | 30 + client/src/pages/NotesPage.vue | 252 +++--- client/src/pages/QuickPage.vue | 4 +- client/src/pages/SharePage.vue | 103 ++- client/src/router/index.js | 9 +- client/src/stores/mainStore.js | 24 +- server/index.js | 110 ++- server/models/Attachment.js | 4 +- server/models/Note.js | 130 ++-- server/models/ShareNote.js | 296 ++++--- server/models/User.js | 29 +- server/routes/noteController.js | 35 +- server/routes/publicController.js | 13 +- server/routes/userController.js | 37 +- 31 files changed, 1605 insertions(+), 1095 deletions(-) create mode 100644 client/src/mixins/SquireButtonFunctions.js create mode 100644 client/src/pages/NotFoundPage.vue diff --git a/client/src/Helpers.js b/client/src/Helpers.js index 26e2703..c789180 100644 --- a/client/src/Helpers.js +++ b/client/src/Helpers.js @@ -3,6 +3,11 @@ import Vue from 'vue' const helpers = {} helpers.timeAgo = (time) => { + + if(time.toString().length >= 13){ + time = Math.round(time/1000) + } + const time_formats = [ [ 60, 'seconds', 1 ], [ 120, '1 minute ago', '1 minute from now' ], diff --git a/client/src/assets/semantic-helper.css b/client/src/assets/semantic-helper.css index cf4cf55..bf28844 100644 --- a/client/src/assets/semantic-helper.css +++ b/client/src/assets/semantic-helper.css @@ -17,10 +17,11 @@ :root { - --background_color: #fff; + --body_bg_color: #f5f6f7; + --small_element_bg_color: #fff; --text_color: #3d3d3d; - --outline_color: rgba(34,36,38,.15); - --border_color: rgba(34,36,38,.20); + --dark_border_color: #DFE1E6; + --border_color: #DFE1E6; /* Global purple menu styles */ --menu-border: #534c68; @@ -34,7 +35,9 @@ html { /*scrollbar-width: none;*/ } - +a:hover { + text-decoration: underline; +} div.ui.basic.segment.no-fluf-segment { margin-top: 0px; } @@ -59,21 +62,21 @@ div.ui.basic.segment.no-fluf-segment { /* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/ body { color: var(--text_color); - background-color: var(--background_color); + background-color: var(--body_bg_color); font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif; } .ui.segment { color: var(--text_color); - background-color: var(--background_color); - border-color: var(--border_color); + background-color: var(--small_element_bg_color); + border-color: var(--dark_border_color); } .button-sub { display: inline-block; width: 100%; font-size: 0.9em; - color: white; - opacity: 0.8; + color: grey; + opacity: 0.9; padding: 4px 0 0 0; text-align: center; } @@ -83,71 +86,43 @@ body { .ui.form textarea:not([type]), .ui.form textarea:not([type]):focus { color: var(--text_color); - background-color: var(--background_color); - border-color: var(--border_color); + background-color: var(--small_element_bg_color); + border-color: var(--dark_border_color); } .ui.basic.label, .ui.header, .ui.header div.sub.header { color: var(--text_color); - background-color: var(--background_color); - border-color: var(--border_color); + background-color: transparent; + border-color: var(--dark_border_color); } .ui.icon.input > i.icon { color: var(--text_color); } div.ui.basic.green.label { - background-color: var(--background_color) !important; + background-color: var(--small_element_bg_color) !important; } .ui.basic.button, .ui.basic.buttons .button { - background-color: var(--background_color) !important; + background-color: var(--small_element_bg_color) !important; color: var(--text_color) !important; border: 1px solid; - border-color: var(--border_color) !important; + border-color: var(--dark_border_color) !important; box-shadow: none; } .ui.basic.button:focus, .ui.basic.button:hover { - background-color: var(--background_color) !important; + background-color: var(--small_element_bg_color) !important; color: var(--text_color) !important; box-shadow: none; } .ui.tabular.menu .item { - background-color: var(--background_color) !important; + background-color: var(--small_element_bg_color) !important; color: var(--text_color) !important; } .ui.tabular.menu .item.active { - background-color: var(--background_color) !important; + background-color: var(--small_element_bg_color) !important; color: var(--text_color) !important; - border-color: var(--border_color) !important; + border-color: var(--dark_border_color) !important; } /* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/ -/* Styles for public display pages */ -.fun { - color: rgba(0, 0, 0, 0.87); - color: var(--text_color); -} -.fun h1 { - font-size: 2em; -} -.fun h2 { - font-size: 1.9em; -} -.fun h3 { - font-size: 1.7em; -} -.fun p { - /*font-size: 1.5em;*/ -} -.fun blockquote { - border-left: 5px solid cornflowerblue; - padding-left: 25px; - margin-left: 5px; -} -/* Styles for public display pages */ - -a:hover { - text-decoration: underline; -} - /*// // Purple Global Menu //*/ @@ -255,6 +230,8 @@ a:hover { word-wrap: break-word; /*border-bottom: 1px solid #ccc;*/ scrollbar-width: none; + scrollbar-color: transparent transparent; + caret-color: #21BA45; } .squire-box::selection, .squire-box::-moz-selection { @@ -271,19 +248,15 @@ a:hover { .squire-box a { cursor: pointer; } - .note-card-text i:not(.icon), - .squire-box i { - padding: 0.5em 0.99em; - border-radius: 1px; - display: inline-block; - font-style: normal; - background-color: rgba(113, 113, 113, 0.1); - } .night-mode .note-card-text i:not(.icon), .night-mode .squire-box i { background-color: rgba(255, 255, 255, 0.2); } + .note-card-text pre, + .squire-box pre { + word-wrap: break-word; + } .note-card-text p, .squire-box p { margin-bottom: 0; diff --git a/client/src/components/AttachmentDisplayCard.vue b/client/src/components/AttachmentDisplayCard.vue index 498beea..1b27f0a 100644 --- a/client/src/components/AttachmentDisplayCard.vue +++ b/client/src/components/AttachmentDisplayCard.vue @@ -6,6 +6,7 @@ display: inline-block; border: 1px solid; border-color: var(--border_color); + background-color: var(--small_element_bg_color); border-radius: 4px; margin: 0 0 15px; max-height: 10000px; diff --git a/client/src/components/FastFilters.vue b/client/src/components/FastFilters.vue index 0d57a4d..cc80673 100644 --- a/client/src/components/FastFilters.vue +++ b/client/src/components/FastFilters.vue @@ -40,7 +40,7 @@ let filter = {} filter[option] = 1 - this.$bus.$emit('update_fast_filters', filter) + // this.$bus.$emit('update_fast_filters', filter) } } } @@ -62,7 +62,7 @@ .filter-menu { color: var(--text_color); - background-color: var(--background_color); + background-color: var(--small_element_bg_color); border: 1px solid; diff --git a/client/src/components/GlobalSiteMenu.vue b/client/src/components/GlobalSiteMenu.vue index 1e24aea..f3f85f3 100644 --- a/client/src/components/GlobalSiteMenu.vue +++ b/client/src/components/GlobalSiteMenu.vue @@ -20,16 +20,16 @@ } .menu-logo-display { width: 25px; - margin: 5px 0 0 34px; + margin: 5px 0 0 42px; display: inline-block; } .menu-item { color: #fff; - padding: 0.8em 10px 0.8em 10px; + padding: 9px 10px; display: inline-block; width: 100%; - font-size: 1.15em; + font-size: 1.1em; box-sizing: border-box; } .menu-item i.icon { @@ -76,7 +76,7 @@ left: 0; right: 0; z-index: 999; - background-color: var(--background_color); + background-color: var(--small_element_bg_color); border-bottom: 1px solid; border-color: var(--border_color); padding: 5px 1rem 5px; @@ -117,9 +117,9 @@ - + @@ -184,6 +184,16 @@
+ + + @@ -218,7 +228,7 @@ Black Theme - Night Theme + Flux Theme Light Theme
@@ -257,7 +267,7 @@ }, data: function(){ return { - version: '2.2.3', + version: '2.3.4', username: '', collapsed: false, mobile: false, @@ -354,27 +364,17 @@ //Reloads note page to initial state this.$bus.$emit('note_reload') }, - updateFastFilters(index){ + updateFastFilters(filterIndex){ //A little hacky, brings user to notes page then filters on click - if(this.$route.name != 'NotesPage'){ + if(this.$route.name != 'Note Page'){ this.$router.push('/notes') setTimeout( () => { - this.updateFastFilters(index) + this.$bus.$emit('update_fast_filters', filterIndex) }, 500 ) + } else { + this.$bus.$emit('update_fast_filters', filterIndex) } - - const options = [ - 'withLinks', // 'Only Show Notes with Links' - 'withTags', // 'Only Show Notes with Tags' - 'onlyArchived', //'Only Show Archived Notes' - 'onlyShowSharedNotes', //Only show shared notes - ] - - let filter = {} - filter[options[index]] = 1 - - this.$bus.$emit('update_fast_filters', filter) }, reloadPage(){ location.reload(true) diff --git a/client/src/components/LoadingIconComponent.vue b/client/src/components/LoadingIconComponent.vue index ed1e40f..b8f5187 100644 --- a/client/src/components/LoadingIconComponent.vue +++ b/client/src/components/LoadingIconComponent.vue @@ -33,29 +33,17 @@ export default { name: 'LoadingIcon', props:[ 'message' ], - data () { - return { - items: [] - } - }, - beforeMount(){ - - }, - mounted(){ - }, - methods: { - onClickTag(index){ - console.log('yup') - }, - } } @@ -120,7 +124,7 @@

- An easy, free, secure Note App + A free, secure Note App

@@ -146,7 +150,7 @@

- Sign Up Now - Only a Username and Password are required.

+ Sign Up Now - Only a Username and Password required
@@ -155,7 +159,7 @@
-

Solid Scribe is an online note application that focuses on ease of use and security

+

Solid Scribe is a browser based note application that focuses on ease of use while keeping your data private

Tools to organize and collaborate on notes while maintaining security and respecting your privacy.

@@ -163,24 +167,25 @@
-
+
- Pruning the mind garden + marketing mumbo jumbo +
-

Tools to organize thousands of notes

-

Tag, Pin, Color, Archive, Attach Images and Search notes or links in notes

+

All Note text is encrypted

+

Only you can read your notes. Employees can not snoop your account. No one can read your data for advertising. Note text is completely unreadable without your password.

-
+
-

Privacy through Encryption

-

All notes are encrypted. No one can read your notes, even if they steal the data from the database.

+

Organize your notes

+

Tag, Pin, Color, Archive, Attach Images, Share Encrypted Notes and Search

- marketing mumbo jumbo + Pruning the mind garden
@@ -189,7 +194,7 @@ Girl falling into the spiral of digital chaos
-

Extremely accessible

+

Extremely accessible - Nothing to install

Works on mobile or desktop browsers.
Behaves like an installed app on mobile phones.

@@ -198,7 +203,7 @@

Secure Search

-

Keyword search using an encrypted search index helps you find what you need without compromising security

+

Keyword search using an encrypted search index helps you find what you need without compromising security.

Hypercube of Solutions @@ -210,16 +215,16 @@ Scheme for planetary destruction
-

Embrace the Void

-

Remove unnecessary clutter for your brain and save it to the cloud, allowing you to easily embrace the gaping abyss

+

Create Lists with Check Boxes

+

Todo lists are supported. With options to removed checked items, sort by completed and un-check all.

-

Space for Growth

-

Groom a clear path for new expressions and innovations. Elevate your being and lower your cholesterol

+

Powerful Text Editing

+

A plethora of editing tools are provided for coloring, underlining, bolding, attaching images and more.

Endless progress at the cost of sanity and health @@ -231,13 +236,13 @@ Shrunken man near giant tablet
-

Become your Data

-

We exist as electrical impulses, no different from data on a computer

+

Secure Data Sharing

+

Share notes with friends without compromising privacy. The data remains encrypted with a shared password for you and people you invite to view it.

-
+
@@ -282,20 +287,18 @@


- OR


- View real information about this site + About

- What is this really? + Why Does this App exist?

-

Its just a little web app for taking notes. This page is mocking the "over the top" marketing sites use to sell their products.

This App exists because I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.

diff --git a/client/src/pages/LoginPage.vue b/client/src/pages/LoginPage.vue index fe73526..1056ed7 100644 --- a/client/src/pages/LoginPage.vue +++ b/client/src/pages/LoginPage.vue @@ -10,7 +10,7 @@
-
+

@@ -44,53 +44,10 @@ data () { return { enabled: false, - username: '', - password: '' } }, methods: { - submit(){ - //Both fields are required - if(this.username <= 0){ - return false - } - if(this.password <= 0){ - return false - } - - let vm = this - - let data = { - username: this.username, - password: this.password - } - - axios.post('/api/user/login', data) - .then(response => { - if(response.data.success){ - - const token = response.data.token - const username = response.data.username - const masterKey = response.data.masterKey - - this.$store.commit('setLoginToken', {token, username, masterKey}) - - //Setup socket io after user logs in - this.$io.emit('user_connect', token) - - //Redirect user to notes section after login - this.$router.push('/notes') - } else { - // this.password = '' - this.$bus.$emit('notification', 'Incorrect Username or Password') - vm.$store.commit('destroyLoginToken') - } - }) - .catch(error => { - this.$bus.$emit('notification', 'Incorrect Username or Password') - }) - } } } \ No newline at end of file diff --git a/client/src/pages/NotFoundPage.vue b/client/src/pages/NotFoundPage.vue new file mode 100644 index 0000000..dacf79f --- /dev/null +++ b/client/src/pages/NotFoundPage.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/client/src/pages/NotesPage.vue b/client/src/pages/NotesPage.vue index f2636af..2eab942 100644 --- a/client/src/pages/NotesPage.vue +++ b/client/src/pages/NotesPage.vue @@ -7,27 +7,19 @@
+ +
+ +
-
+
- Shared Notes - - {{ $store.getters.totals['unreadNotes'] }} - -
- -
- Archived - -
- -
- + Inbox + +{{ $store.getters.totals['youGotMailCount'] }}
-
- -
-
@@ -75,7 +61,7 @@
-

Trash +

Trash ({{ $store.getters.totals['trashedNotes'] }})
@@ -88,6 +74,25 @@

Shared Notes

+
+
Tags ({{ tagSuggestions.length }})
+
+ + {{ tag.text }} +
+
+ + +
+
Files ({{ foundAttachments.length }})
+ +
+
@@ -123,18 +128,6 @@

- - -
-

Found in Files ({{ foundAttachments.length }})

- -
-
@@ -170,7 +163,7 @@ data () { return { initComponent: true, - commonTags: [], + tagSuggestions:[], searchTerm: '', searchResultsCount: 0, searchTags: [], @@ -191,13 +184,6 @@ //Clear button is not visible showClear: false, initialPostData: null, - currentPostData: null, - - containsNormalNotes: 0, - containsPinnednotes: 0, - containsTextResults: 0, - // containsTagResults: 0, - // containsAttachmentResults: 0, //Currently open notes in app activeNoteId1: null, @@ -214,8 +200,8 @@ sectionData: { 'pinned': ['thumbtack', 'Pinned'], 'archived': ['archive', 'Archived'], - 'shared': ['envelope outline', 'Received Notes'], - 'sent': ['paper plane outline', 'Shared Notes'], + 'shared': ['envelope outline', 'Inbox'], + 'sent': ['paper plane outline', 'Sent Notes'], 'notes': ['file','Notes'], 'highlights': ['paragraph', 'Found In Text'], 'trashed': ['poop', 'Trashed Notes'], @@ -275,13 +261,12 @@ this.$store.dispatch('fetchAndUpdateUserTotals') //Close note event - this.$bus.$on('close_active_note', ({position, noteId, modified}) => { + this.$bus.$on('close_active_note', ({noteId, modified}) => { this.closeNote() - if(modified){ - this.$store.dispatch('fetchAndUpdateUserTotals') - this.updateSingleNote(parseInt(noteId)) - } + this.$store.dispatch('fetchAndUpdateUserTotals') + //Focus and animate if modified + this.updateSingleNote(parseInt(noteId), modified) }) this.$bus.$on('note_deleted', (noteId) => { @@ -295,15 +280,12 @@ return } }) - }) + }) }) - this.$bus.$on('update_fast_filters', newFilter => { - this.fastFilters = newFilter - //Fast filters always return all the results and tags - this.search(true, this.batchSize, false).then( () => { - // return - }) + this.$bus.$on('update_fast_filters', filterIndex => { + + this.updateFastFilters(filterIndex) }) //Event to update search from other areas @@ -312,8 +294,18 @@ this.search(true, this.batchSize) .then( () => { - console.log('Search attachments disabled for now') - // this.searchAttachments() + this.searchAttachments() + + const postData = { + 'tagText':this.searchTerm.trim() + } + + this.tagSuggestions = [] + axios.post('/api/tag/suggest', postData) + .then( response => { + + this.tagSuggestions = response.data + }) // return }) @@ -365,6 +357,9 @@ //Loads initial batch and tags this.reset() + + // this.search(true, this.firstLoadBatchSize, false) + // .then( r => this.search(false, this.batchSize, true)) }, methods: { toggleTitleView(){ @@ -390,7 +385,7 @@ if(this.activeNoteId1 == null){ this.activeNoteId1 = id this.activeNote1Position = 0 //Middel of page - this.$router.push('/notes/open/'+this.activeNoteId1) + this.$router.push('/notes/open/'+this.activeNoteId1).catch(e => { console.log(e) }) return } }, @@ -417,24 +412,18 @@ clearTimeout(this.loadingBatchTimeout) this.loadingBatchTimeout = setTimeout(() => { - //Distance to bottom of page - const bottomOfWindow = - Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) - + window.innerHeight + //Detect distance scrolled down the page + const scrolledDown = window.pageYOffset + window.innerHeight + //Get height of div to properly detect scroll distance down + const height = document.getElementById('app').scrollHeight - //height of page - const offsetHeight = this.$refs.content.clientHeight - - //Determine percentage down the page - const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 ) - - //If greater than 80 of the way down the page, load the next batch - if(percentageDown >= 65 && this.scrollLoadEnabled){ + //Load if less than 500px from the bottom + if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){ this.search(false, this.batchSize, true) } - }, 50) + }, 30) return @@ -491,7 +480,10 @@ noteId = parseInt(noteId) //Find local note, if it exists; continue - + let note = null + if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){ + note = this.$refs['note-'+noteId][0].note + } //Lookup one note using passed in ID const postData = { @@ -508,60 +500,62 @@ //Pull note data out of note set let newNote = results.data.notes[0] - let foundNote = false if(newNote === undefined){ return } - //Find Just updated note and modify all its attributes - Object.keys(this.noteSections).forEach(key => { + if(note && newNote){ - this.noteSections[key].forEach( (note,index) => { + //Don't move notes that were not changed + if(note.updated == newNote.updated){ + return + } - if(note.id == noteId){ - foundNote = true + //go through each prop and update it with new values + Object.keys(newNote).forEach(prop => { + note[prop] = newNote[prop] + }) - //Don't move notes that were not changed - if(note.updated == newNote.updated){ - // return - } + //Push new note to front if its modified + if(focuseAndAnimate){ - //Compare note tags, if they changed, reload tags - if(newNote.tag_count != note.tag_count){ - - } - - //go through each prop and update it with new values - Object.keys(newNote).forEach(prop => { - note[prop] = newNote[prop] - }) - - //Remove note from location and push to front - this.noteSections[key].splice(index, 1) - this.noteSections[key].unshift(note) - - this.$nextTick( () => { - - //Trigger close animation on note - if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && focuseAndAnimate){ - this.$refs['note-'+noteId][0].justClosed() + // Find note, in section, move to front + Object.keys(this.noteSections).forEach( key => { + this.noteSections[key].forEach( (searchNote, index) => { + if(searchNote.id == noteId){ + //Remove note from location and push to front + this.noteSections[key].splice(index, 1) + this.noteSections[key].unshift(note) + return } }) + }) - return - } - }) - }) + this.$nextTick( () => { + //Trigger close animation on note + this.$refs['note-'+noteId][0].justClosed() + }) + } + + } //New notes don't exist in list, push them to the front - if(!foundNote){ + if(note == null){ this.noteSections.notes.unshift(newNote) + //Trigger close animation on note + if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){ + this.$refs['note-'+noteId][0].justClosed() + } } + //Trigger section rebuild this.rebuildNoteCategorise() }) - .catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') }) + .catch(error => { + console.log(error) + this.$bus.$emit('notification', 'Failed to Update Note') + }) }, searchAttachments(){ axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm}) @@ -570,13 +564,13 @@ }) .catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') }) }, - search(showLoading = true, notesInNextLoad = null, mergeExisting = false){ + search(showLoading = true, notesInNextLoad = 10, mergeExisting = false){ return new Promise((resolve, reject) => { //Don't double load note batches if(this.loadingInProgress){ - console.log('Loading in progress, cancel operation') - return resolve() + console.log('Loading already in progress') + return resolve(false) } //Reset a lot of stuff if we are not merging batches @@ -585,7 +579,6 @@ this.noteSections[key] = [] }) this.batchOffset = 0 // Reset batch offset if we are not merging note batches - // this.commonTags = [] //Don't reset tags, if search returns tags, they will be set } this.searchResultsCount = 0 @@ -628,11 +621,6 @@ //Enable or disable scroll loading this.scrollLoadEnabled = response.data.notes.length > 0 - //Mush the two new sets of data together (set will be empty is reset is on) - // if(response.data.tags.length > 0){ - // this.commonTags = response.data.tags - // } - if(response.data.total > 0){ this.searchResultsCount = response.data.total } @@ -742,33 +730,22 @@ this.scrollLoadEnabled = true this.searchTerm = '' this.searchTags = [] + this.tagSuggestions = [] this.fastFilters = {} - this.updateFastFilters(5) this.foundAttachments = [] //Remove all attachments - this.$bus.$emit('reset_fast_filters') - //Load initial batch, then tags, then other batch - this.search(true, this.firstLoadBatchSize) - .then( () => { - //Load a larger batch once first batch has loaded - return this.search(false, this.batchSize, true) - }) + this.$bus.$emit('reset_fast_filters') + this.updateFastFilters(5) //This loads notes + }, updateFastFilters(index){ //clear out tags this.searchTags = [] + this.tagSuggestions = [] this.loadingInProgress = false this.searchTerm = '' - this.$bus.$emit('reset_fast_filters') - - //A little hacky, brings user to notes page then filters on click - if(this.$route.name != 'Note Page'){ - this.$router.push('/notes') - setTimeout( () => { - this.updateFastFilters(index) - }, 500 ) - } + this.$bus.$emit('reset_fast_filters') //Clear out search const options = [ 'withLinks', // 'Only Show Notes with Links' @@ -782,7 +759,10 @@ let filter = {} filter[options[index]] = 1 - this.$bus.$emit('update_fast_filters', filter) + this.fastFilters = filter + //Fetch First batch of notes with new filter + this.search(true, this.firstLoadBatchSize, false) + .then( r => this.search(false, this.batchSize, true)) } } } diff --git a/client/src/pages/QuickPage.vue b/client/src/pages/QuickPage.vue index b9bdad9..aee5532 100644 --- a/client/src/pages/QuickPage.vue +++ b/client/src/pages/QuickPage.vue @@ -12,11 +12,11 @@

-
+
- New Quick Note + New Scratch Pad
diff --git a/client/src/pages/SharePage.vue b/client/src/pages/SharePage.vue index cf02cae..de1be7c 100644 --- a/client/src/pages/SharePage.vue +++ b/client/src/pages/SharePage.vue @@ -1,9 +1,49 @@ @@ -15,36 +55,51 @@ name: 'SharePage', data(){ return { - noteText: null, - color: '#000' + title: '', + text: '', + failText: '', + styleObject:{}, } }, beforeMount(){ - //Mount notes on load if note ID is set - if(this.$route.params && this.$route.params.id){ - const id = this.$route.params.id - this.openNote(id) - } + + //You can put something here for live updates + // this.$io.on + + this.openNote() + }, methods:{ - openNote(noteId){ - axios.post('/api/public/note', {'noteId': noteId}) - .then( response => { + fail(){ + this.failText = 'Failed to open Shared Note' + this.$bus.$emit('notification', 'Failed to Open Shared Note') + }, + openNote(){ - let colors = JSON.parse(response.data.color) + const noteId = this.$route.params.id + const sharedKey = this.$route.params.token - if(colors && colors.noteBackground){ - document.body.style.background = colors.noteBackground + axios.post('/api/public/opensharednote', {noteId, sharedKey}) + .then( ({data}) => { + + if(data.success){ + this.title = data.title + this.text = data.text + this.styleObject = data.styleObject + } else { + this.fail() } - if(colors && colors.noteText){ - this.color = colors.noteText - } - - this.noteText = response.data.text }) - .catch(error => { this.$bus.$emit('notification', 'Failed to Open Public Note') }) + .catch(error => { this.fail() }) } } } - \ No newline at end of file + + + \ No newline at end of file diff --git a/client/src/router/index.js b/client/src/router/index.js index f1e52ca..e3636f4 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -10,6 +10,7 @@ const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/Shar const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage') const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage') const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage') +const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage') Vue.use(Router) @@ -52,7 +53,7 @@ export default new Router({ component: HelpPage }, { - path: '/share/:id', + path: '/public/note/:id/:token', name: 'Share', meta: {title:'Shared'}, component: SharePage @@ -81,5 +82,11 @@ export default new Router({ meta: {title:'Attachments by Type'}, component: AttachmentsPage }, + { + path: '*', + name: 'Page Not Found', + meta: {title:'404 Page Not Found'}, + component: NotFoundPage + }, ] }) diff --git a/client/src/stores/mainStore.js b/client/src/stores/mainStore.js index f4e9dfe..0a697df 100644 --- a/client/src/stores/mainStore.js +++ b/client/src/stores/mainStore.js @@ -45,34 +45,36 @@ export default new Vuex.Store({ const themes = { 'white':{ - 'background_color': '#fff', + 'body_bg_color': '#f5f6f7', + 'small_element_bg_color': '#fff', 'text_color': '#3d3d3d', - 'outline_color': 'rgba(34,36,38,0.15)', - 'border_color': 'rgba(34,36,38,0.20)', + 'dark_border_color': '#DFE1E6', + 'border_color': '#DFE1E6', 'menu-accent': '#cecece', 'menu-text': '#5e6268', }, 'black':{ - 'background_color': '#000', + 'body_bg_color': '#000', + 'small_element_bg_color': '#000', 'text_color': '#FFF', - 'outline_color': '#FFF', - 'border_color': 'rgba(255, 255, 255, 0.70)', + 'dark_border_color': '#ACACAC', //Lighter color to accent elemnts user can interact with + 'border_color': '#555', 'menu-accent': '#626262', 'menu-text': '#d9d9d9', }, 'night':{ - 'background_color': '#000', + 'body_bg_color': '#000', + 'small_element_bg_color': '#000', 'text_color': '#a98457', - 'outline_color': '#a98457', - 'border_color': 'rgba(255, 255, 255, 0.31)', + 'dark_border_color': '#a98457', + 'border_color': '#555', 'menu-accent': '#626262', - 'menu-text': '#d9d9d9', + 'menu-text': '#a69682', }, } //Catch values not in set - const totalThemes = Object.keys(themes).length state.nightMode++ if(state.nightMode > totalThemes-1){ diff --git a/server/index.js b/server/index.js index b040c57..bfbd3e8 100644 --- a/server/index.js +++ b/server/index.js @@ -40,11 +40,10 @@ var io = require('socket.io')(http, { //Set socket IO as a global in the app global.SocketIo = io +let noteDiffs = {} io.on('connection', function(socket){ - // console.log('New user ', socket.id) - //When a user connects, add them to their own room // This allows the server to emit events to that specific user // access socket.io in the controller with SocketIo global @@ -58,14 +57,43 @@ io.on('connection', function(socket){ }) }) - socket.on('join_room', roomId => { - // console.log('Join room ', roomId) - socket.join(roomId) + socket.on('join_room', rawTextId => { + // Join user to rawtextid room when they enter + socket.join(rawTextId) - const usersInRoom = io.sockets.adapter.rooms[roomId] + //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 }) + + //Emit all sorted diffs to user + socket.emit('past_diffs', noteDiffs[rawTextId]) + } else { + socket.emit('past_diffs', null) + } + + const usersInRoom = io.sockets.adapter.rooms[rawTextId] if(usersInRoom){ - // console.log('Users in room', usersInRoom.length) - io.to(roomId).emit('update_user_count', usersInRoom.length) + //Update users in room count + io.to(rawTextId).emit('update_user_count', usersInRoom.length) + + //Debugging text + console.log('Note diff object') + console.log(noteDiffs) + + + let noteDiffKeys = Object.keys(noteDiffs) + let totalDiffs = 0 + noteDiffKeys.forEach(diffSetKey => { + if(noteDiffs[diffSetKey]){ + totalDiffs += noteDiffs[diffSetKey].length + } + }) + + console.log('Total notes in limbo -> ', noteDiffKeys.length) + console.log('Total Diffs for all notes -> ', totalDiffs) + } }) @@ -83,13 +111,41 @@ io.on('connection', function(socket){ socket.on('note_diff', data => { + //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) + + //Remove duplicate diffs if they exist + for (var i = noteDiffs[noteId].length - 1; i >= 0; i--) { + + let pastDiff = noteDiffs[noteId][i] + + for (var j = noteDiffs[noteId].length - 1; j >= 0; j--) { + let currentDiff = noteDiffs[noteId][j] + + if(i == j){ + continue + } + + if(currentDiff.diff == pastDiff.diff || currentDiff.time == pastDiff.time){ + console.log('Removing Duplicate') + noteDiffs[noteId].splice(i,1) + } + } + } + //Each user joins a room when they open the app. - io.in(data.id).clients((error, clients) => { + io.in(noteId).clients((error, clients) => { if (error) throw error; + //Go through each client in note room and send them the diff clients.forEach(socketId => { if(socketId != socket.id){ - io.to(socketId).emit('incoming_diff', data.diff) + io.to(socketId).emit('incoming_diff', data) } }) @@ -97,6 +153,38 @@ io.on('connection', function(socket){ }) + socket.on('truncate_diffs_at_save', checkpoint => { + + let diffSet = noteDiffs[checkpoint.rawTextId] + if(diffSet && diffSet.length > 0){ + + //Make sure all diffs are sorted before cleaning + noteDiffs[checkpoint.rawTextId].sort((a,b) => { return a.time - b.time }) + + // Remove all diffs until it reaches the current hash + let sliceTo = 0 + for (var i = 0; i < diffSet.length; i++) { + if(diffSet[i].hash == checkpoint){ + sliceTo = i + break + } + } + + + noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo) + + if(noteDiffs[checkpoint.rawTextId].length == 0){ + delete noteDiffs[checkpoint.rawTextId] + } + //Debugging + else { + console.log('Diffset after save') + console.log(noteDiffs[checkpoint.rawTextId]) + } + + } + }) + socket.on('disconnect', function(){ // console.log('user disconnected'); }); @@ -139,7 +227,7 @@ app.use(function(req, res, next){ const printResults = false let UserTest = require('@models/User') let NoteTest = require('@models/Note') -UserTest.keyPairTest('genMan2', '1', printResults) +UserTest.keyPairTest('genMan12', '1', printResults) .then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults)) .then( message => { if(printResults) console.log(message) diff --git a/server/models/Attachment.js b/server/models/Attachment.js index 2bc99e8..80f0c8a 100644 --- a/server/models/Attachment.js +++ b/server/models/Attachment.js @@ -280,9 +280,7 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => { Attachment.scrapeUrlsCreateAttachments(userId, noteId, foundUrls).then( freshlyScrapedText => { //Once everything is done being scraped, emit new attachment events - if(io){ - io.to(userId).emit('update_counts') - } + SocketIo.to(userId).emit('update_counts') solrAttachmentText += freshlyScrapedText resolve(solrAttachmentText) diff --git a/server/models/Note.js b/server/models/Note.js index ecb7047..6e15a9d 100644 --- a/server/models/Note.js +++ b/server/models/Note.js @@ -72,7 +72,7 @@ Note.test = (userId, masterKey, printResults) => { if(printResults) console.log('Test: Normal Note Search Index - Pass') } else { console.log('Test: Search Index - Fail') } - return ShareNote.migrateNoteToShared(userId, testNoteId, shareUserId, masterKey) + return ShareNote.addUserToSharedNote(userId, testNoteId, shareUserId, masterKey) }) .then(({success, shareUserId, sharedUserNoteId}) => { @@ -134,13 +134,13 @@ Note.test = (userId, masterKey, printResults) => { if(printResults) console.log('Test: Delete Shared Note - Pass') - return ShareNote.migrateNoteToShared(userId, testNoteId2, shareUserId, masterKey) + return ShareNote.addUserToSharedNote(userId, testNoteId2, shareUserId, masterKey) }) .then(({success, shareUserId, sharedUserNoteId}) => { if(printResults) console.log('Test: Created Another New Shared Note - pass') - return ShareNote.removeUserFromShared(userId, testNoteId2, sharedUserNoteId, masterKey) + return ShareNote.removeUserFromSharedNote(userId, testNoteId2, sharedUserNoteId, masterKey) }) .then(() => { @@ -162,75 +162,6 @@ Note.test = (userId, masterKey, printResults) => { return resolve('Test: Complete ---') }) - }) -} - -//User doesn't have an encrypted note set. Encrypt all notes -Note.encryptEveryNote = (userId, masterKey) => { - - return new Promise((resolve, reject) => { - - //Select all the user notes - db.promise().query(` - SELECT * FROM note - JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) - WHERE salt IS NULL AND user_id = ? AND shared = 0`, [userId]) - .then((rows, fields) => { - - let foundNotes = rows[0] - console.log('Encrypting user notes ',rows[0].length) - - // return resolve(true) - - let allTheUpdates = [] - let timeoutAdder = 0 - foundNotes.forEach(note => { - timeoutAdder += 100 - const newUpdate = new Promise((resolve, reject) => { - setTimeout(() => { - console.log('Encrypting Note ', note.id) - - const created = Math.round((+new Date)/1000) - const salt = cs.createSmallSalt() - - const noteText = note.text - const noteTitle = note.title - - const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)]) - const noteSnippet = cs.encrypt(masterKey, salt, snippet) - - const textObject = JSON.stringify([noteTitle, noteText]) - const encryptedText = cs.encrypt(masterKey, salt, textObject) - - db.promise() - .query('UPDATE note_raw_text SET title = ?, text = ?, snippet = ?, salt = ? WHERE id = ?', - [null, encryptedText, noteSnippet, salt, note.note_raw_text_id]) - .then(() => { - resolve(true) - }) - }, timeoutAdder) - }) - allTheUpdates.push(newUpdate) - }) - - Promise.all(allTheUpdates).then(done => { - - console.log('Indexing first 100') - return Note.reindex(userId, masterKey) - - }).then(results => { - - console.log('Done') - resolve(true) - }) - - }) - - - - - - }) } @@ -490,7 +421,8 @@ Note.reindex = (userId, masterKey, removeId = null) => { Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, hash, masterKey) => { return new Promise((resolve, reject) => { - const now = Math.round((+new Date)/1000) + // const now = Math.round((+new Date)/1000) + const now = +new Date let noteSnippet = '' @@ -691,10 +623,6 @@ Note.delete = (userId, noteId, masterKey = null) => { // delete tags return db.promise() .query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId]) - }) - .then((rows, fields) => { - - }) .then((rows, fields) => { @@ -706,6 +634,8 @@ Note.delete = (userId, noteId, masterKey = null) => { } }) + SocketIo.to(userId).emit('update_counts') + if(masterKey){ //Remove note ID from index Note.reindex(userId, masterKey, [noteId]) @@ -803,6 +733,7 @@ Note.get = (userId, noteId, masterKey) => { note.archived, note.trashed, note.color, + note.shared, note.encrypted_share_password_key, count(distinct attachment.id) as attachment_count, note.note_raw_text_id as rawTextId, @@ -845,8 +776,8 @@ Note.get = (userId, noteId, masterKey) => { db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId]) //Return note data - delete noteData.salt //remove salt from return data - delete noteData.encrypted_share_password_key + // delete noteData.salt //remove salt from return data + // delete noteData.encrypted_share_password_key noteData.lockedOut = noteLockedOut resolve(noteData) @@ -859,17 +790,48 @@ Note.get = (userId, noteId, masterKey) => { } //Public note share action -> may not be used -Note.getShared = (noteId) => { +Note.getShared = (noteId, sharedKey) => { return new Promise((resolve, reject) => { db.promise() - .query('SELECT text, color FROM note WHERE id = ? AND shared = 1 LIMIT 1', [noteId]) + .query(` + SELECT + note_raw_text.text, + note_raw_text.salt, + note_raw_text.updated as updated, + note.id, + note.user_id, + note.created, + note.pinned, + note.archived, + note.trashed, + note.color, + note.shared, + note.encrypted_share_password_key, + note.note_raw_text_id as rawTextId + FROM note + JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) + WHERE note.id = ? LIMIT 1`, [noteId]) .then((rows, fields) => { - //Return note data - resolve(rows[0][0]) + let noteData = rows[0][0] + + const decipheredText = cs.decrypt(sharedKey, noteData.salt, noteData.text) + if(decipheredText == null){ + throw new Error('Unable to decropt note text') + } + + const success = true + const noteObject = JSON.parse(decipheredText) + const title = noteObject[0] + const text = noteObject[1] + const styleObject = JSON.parse(noteData.color) + + return resolve({success, title, text, styleObject}) }) - .catch(console.log) + .catch(error => { + resolve({'success':false}) + }) }) } diff --git a/server/models/ShareNote.js b/server/models/ShareNote.js index 8a62873..04b7991 100644 --- a/server/models/ShareNote.js +++ b/server/models/ShareNote.js @@ -12,19 +12,19 @@ let ShareNote = module.exports = {} const crypto = require('crypto') const cs = require('@helpers/CryptoString') -ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => { +ShareNote.addUserToSharedNote = (userId, noteId, shareUserId, masterKey) => { return new Promise((resolve, reject) => { const Note = require('@models/Note') const User = require('@models/User') //generate new random salts and password - const sharedNoteMasterKey = cs.createSmallSalt() + let sharedNoteMasterKey = null let encryptedSharedKey = null //new key for note encrypted with shared users pubic key //Current note object - let note = null + let noteObject = null let publicKey = null db.promise().query('SELECT id FROM user WHERE id = ?', [shareUserId]) @@ -34,20 +34,16 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => { throw new Error('User Does Not Exist') } - return Note.get(userId, noteId, masterKey) + return ShareNote.migrateToShared(userId, noteId, masterKey) }) - .then( noteObject => { + .then(({note, sharedNoteKey})=> { - if(!noteObject){ - throw new Error('Note Not Found') - } - - note = noteObject + sharedNoteMasterKey = sharedNoteKey + noteObject = note return db.promise() .query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, note.rawTextId]) - }) .then((rows, fields) => { @@ -55,46 +51,6 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => { throw new Error('User Already has this note shared with them') } - //All check pass, proceed with sharing note - return User.getPublicKey(userId) - }) - .then( userPublicKey => { - - //Get users public key - publicKey = userPublicKey - - // - // Modify note to have a shared password, encrypt text with this password - // - const sharedNoteSalt = cs.createSmallSalt() - - //Encrypt note text with new password - const textObject = JSON.stringify([note.title, note.text]) - const encryptedText = cs.encrypt(sharedNoteMasterKey, sharedNoteSalt, textObject) - - //Update note raw text with new data - return db.promise() - .query("UPDATE `application`.`note_raw_text` SET `text` = ?, `salt` = ? WHERE (`id` = ?)", - [encryptedText, sharedNoteSalt, note.rawTextId]) - - }) - .then((rows, fields) => { - - //New Encrypted snippet, using new shared password - const sharedNoteSnippetSalt = cs.createSmallSalt() - const snippet = JSON.stringify([note.title, note.text.substring(0, 500)]) - const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, sharedNoteSnippetSalt, snippet) - - //Encrypt shared password for this user - const encryptedSharedKey = crypto.publicEncrypt(publicKey, Buffer.from(sharedNoteMasterKey, 'utf8')).toString('base64') - - //Update note snippet for current user with public key encoded snippet - return db.promise().query('UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 2 WHERE id = ? AND user_id = ?', - [encryptedSnippet, sharedNoteSnippetSalt, encryptedSharedKey, noteId, userId]) - - }) - .then((rows, fields) => { - return User.getPublicKey(shareUserId) }) @@ -102,7 +58,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => { //New Encrypted snippet, using new shared password const newSnippetSalt = cs.createSmallSalt() - const snippet = JSON.stringify([note.title, note.text.substring(0, 500)]) + const snippet = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)]) const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, newSnippetSalt, snippet) //Encrypt shared password for this user @@ -111,7 +67,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => { //Insert new note for shared user return db.promise().query(` INSERT INTO note (user_id, note_raw_text_id, created, color, share_user_id, snippet, snippet_salt, encrypted_share_password_key) VALUES (?,?,?,?,?,?,?,?); - `, [shareUserId, note.rawTextId, note.created, note.color, userId, encryptedSnippet, newSnippetSalt, encryptedSharedKey]) + `, [shareUserId, noteObject.rawTextId, noteObject.created, noteObject.color, userId, encryptedSnippet, newSnippetSalt, encryptedSharedKey]) }) .then((rows, fields) => { @@ -119,7 +75,7 @@ ShareNote.migrateNoteToShared = (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(sharedUserNoteId).emit('update_counts') + SocketIo.to(shareUserId).emit('update_counts') let success = true return resolve({success, shareUserId, sharedUserNoteId}) @@ -133,7 +89,7 @@ ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => { }) } -ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) => { +ShareNote.removeUserFromSharedNote = (userId, noteId, shareNoteUserId, masterKey) => { return new Promise((resolve, reject) => { let rawTextId = null @@ -160,32 +116,9 @@ ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) => //Convert back to normal note if there is only one person with this note if(rows[0][0]['count'] == 1){ - Note.get(userId, noteId, masterKey) - .then(noteObject => { - - const salt = cs.createSmallSalt() - const snippetSalt = cs.createSmallSalt() - - const snippetObj = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)]) - const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj) - - const textObject = JSON.stringify([noteObject.title, noteObject.text]) - const encryptedText = cs.encrypt(masterKey, salt, textObject) - - db.promise() - .query(`UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 0 WHERE id = ? AND user_id = ?`, - [snippet, snippetSalt, null, noteId, userId]) - .then((r,f) => { - - db.promise() - .query('UPDATE note_raw_text SET text = ?, salt = ? WHERE id = ?', - [encryptedText, salt, noteObject.rawTextId]) - .then(() => { - return resolve(true) - }) - - }) - + return ShareNote.migrateToNormal(userId, noteId, masterKey) + .then(results => { + resolve(true) }) } else { @@ -196,9 +129,163 @@ ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) => }) } -// Get users who see a shared note -ShareNote.getUsers = (userId, rawTextId) => { +//Encrypt note with private shared key +ShareNote.migrateToShared = (userId, noteId, masterKey) => { + + const User = require('@models/User') + return new Promise((resolve, reject) => { + + //generate new random password + const sharedNoteMasterKey = cs.createSmallSalt() + let userPublicKey = null + let userPrivateKey = null + let note = null + + User.generateKeypair(userId, masterKey) + .then( ({publicKey, privateKey}) => { + + //Get users public key + userPublicKey = publicKey + userPrivateKey = privateKey + + return Note.get(userId, noteId, masterKey) + }) + .then(noteObject => { + + note = noteObject + + if(note.shared == 2){ + + //Note is already shared, decrypt sharedKey + let sharedNoteKey = null + const encryptedShareKey = note.encrypted_share_password_key + if(encryptedShareKey != null){ + sharedNoteKey = crypto.privateDecrypt(userPrivateKey, + Buffer.from(encryptedShareKey, 'base64') ) + } + + return resolve({note, sharedNoteKey}) + + + } else { + + // + // Update raw_text to have a shared password, encrypt text with this password + // + const sharedNoteSalt = cs.createSmallSalt() + + //Encrypt note text with new password + const textObject = JSON.stringify([note.title, note.text]) + const encryptedText = cs.encrypt(sharedNoteMasterKey, sharedNoteSalt, textObject) + + //Update note raw text with new data + db.promise() + .query("UPDATE `application`.`note_raw_text` SET `text` = ?, `salt` = ? WHERE (`id` = ?)", + [encryptedText, sharedNoteSalt, note.rawTextId]) + .then((rows, fields) => { + + // + // Update snippet using new shared password + // + Save shared password (encrypted with public key) + // + const sharedNoteSnippetSalt = cs.createSmallSalt() + const snippet = JSON.stringify([note.title, note.text.substring(0, 500)]) + const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, sharedNoteSnippetSalt, snippet) + + //Encrypt shared password for this user + const encryptedSharedKey = crypto.publicEncrypt(userPublicKey, Buffer.from(sharedNoteMasterKey, 'utf8')).toString('base64') + + //Update note snippet for current user with public key encoded snippet + return db.promise().query('UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 2 WHERE id = ? AND user_id = ?', + [encryptedSnippet, sharedNoteSnippetSalt, encryptedSharedKey, noteId, userId]) + + }) + .then((rows, fields) => { + return resolve({note, 'sharedNoteKey':sharedNoteMasterKey}) + }) + + } + + }) + + + }) +} +//Remove private shared key, encrypt note with users master password +ShareNote.migrateToNormal = (userId, noteId, masterKey) => { + + return new Promise((resolve, reject) => { + + Note.get(userId, noteId, masterKey) + .then(noteObject => { + + const salt = cs.createSmallSalt() + const snippetSalt = cs.createSmallSalt() + + const snippetObj = JSON.stringify([noteObject.title, noteObject.text.substring(0, 500)]) + const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj) + + const textObject = JSON.stringify([noteObject.title, noteObject.text]) + const encryptedText = cs.encrypt(masterKey, salt, textObject) + + db.promise() + .query(`UPDATE note SET snippet = ?, snippet_salt = ?, encrypted_share_password_key = ?, shared = 0 WHERE id = ? AND user_id = ?`, + [snippet, snippetSalt, null, noteId, userId]) + .then((r,f) => { + + db.promise() + .query('UPDATE note_raw_text SET text = ?, salt = ? WHERE id = ?', + [encryptedText, salt, noteObject.rawTextId]) + .then(() => { + return resolve(true) + }) + + }) + + }) + }) +} + +ShareNote.decryptSharedKey = (userId, noteId, masterKey) => { + return new Promise((resolve, reject) => { + + let userPrivateKey = null + + const User = require('@models/User') + User.generateKeypair(userId, masterKey) + .then( ({publicKey, privateKey}) => { + + userPrivateKey = privateKey + + return Note.get(userId, noteId, masterKey) + }) + .then(noteObject => { + + //Shared notes use encrypted key - decrypt key then return it. + const encryptedShareKey = noteObject.encrypted_share_password_key + + if(encryptedShareKey != null && userPrivateKey != null){ + const currentNoteKey = crypto.privateDecrypt(userPrivateKey, + Buffer.from(encryptedShareKey, 'base64') ) + + return resolve(currentNoteKey) + + } else { + return resolve(null) + } + + }) + }) +} + +// Get users who see a shared note +ShareNote.getShareInfo = (userId, noteId, rawTextId) => { + return new Promise((resolve, reject) => { + + let shareUsers = [] + let shareStatus = 0 + db.promise() .query(` SELECT username, note.id as noteId @@ -211,47 +298,14 @@ ShareNote.getUsers = (userId, rawTextId) => { .then((rows, fields) => { //Return a list of user names - return resolve (rows[0]) + shareUsers = rows[0] + return db.promise().query('SELECT shared FROM note WHERE id = ? AND user_id = ?', [noteId, userId]) }) - }) -} + .then((rows, fields) => { -// Remove a user from a shared note -ShareNote.removeUser = (userId, noteId) => { - return new Promise((resolve, reject) => { - - const Note = require('@models/Note') - - let rawTextId = null - let removeUserId = null - - //note.id = noteId, share_user_id = userId - db.promise() - .query('SELECT note_raw_text_id, user_id FROM note WHERE id = ? AND share_user_id = ?', [noteId, userId]) - .then( (rows, fields) => { - - rawTextId = rows[0][0]['note_raw_text_id'] - removeUserId = rows[0][0]['user_id'] - - //Delete note entry for other user - remove users access - if(removeUserId && Number.isInteger(removeUserId)){ - //Delete this users access to the note - return Note.delete(removeUserId, noteId, masterKey) - - } else { - - return new Promise((resolve, reject) => { resolve(true) }) - } + shareStatus = rows[0][0]['shared'] + return resolve({ shareStatus, shareUsers }) }) - .then(stuff => { - resolve(true) - }) - .catch(error => { - console.log(error) - resolve(false) - }) - - }) } \ No newline at end of file diff --git a/server/models/User.js b/server/models/User.js index 9e251b3..7175412 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -19,15 +19,6 @@ User.login = (username, password) => { .query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName]) .then((rows, fields) => { - // Create New Account - // - if(rows[0].length == 0){ - User.create(lowerName, password) - .then( ({token, userId}) => { - return resolve({ token, userId }) - }) - } - // Login User // if(rows[0].length == 1){ @@ -60,6 +51,8 @@ User.login = (username, password) => { reject('Password does not match database') } }) + } else { + return reject('Incorrect Username or Password') } }) .catch(console.log) @@ -69,7 +62,7 @@ User.login = (username, password) => { //Create user account //Issues login token -User.create = (username, password) => { +User.register = (username, password) => { //For some reason, username won't get into the promise. But password will @TODO figure this out const lowerName = username.toLowerCase().trim() @@ -152,8 +145,8 @@ User.getCounts = (userId) => { SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes, SUM(trashed = 1) AS trashedNotes, SUM(share_user_id IS NULL && trashed = 0) AS totalNotes, - SUM(share_user_id != ? && trashed = 0) AS sharedToNotes, - SUM( (share_user_id != ? && opened IS null && trashed = 0) || (share_user_id != ? && note_raw_text.updated > opened && trashed = 0) ) AS unreadNotes + SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount, + SUM(share_user_id != ? && trashed = 0) AS sharedToNotes FROM note LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) WHERE user_id = ?`, [userId, userId, userId, userId]) @@ -406,12 +399,12 @@ User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => { const randomUsername = Math.random().toString(36).substring(2, 15); const randomPassword = '1' - - User.login(testUserName, password) + + User.register(testUserName, password) .then( ({ token, userId }) => { testUserId = userId - if(printResults) console.log('Test: Create/Login User '+testUserName+' - Pass') + if(printResults) console.log('Test: Register User '+testUserName+' - Pass') return User.getMasterKey(testUserId, password) }) @@ -439,6 +432,12 @@ User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => { //Convert it back to string if(printResults) console.log(publicDeccryptMessage.toString('utf8')) + return User.login(testUserName, password) + }) + .then( ({token, userId}) => { + + if(printResults) console.log('Test: Login New User - Pass') + resolve({testUserId, masterKey}) }) }) diff --git a/server/routes/noteController.js b/server/routes/noteController.js index 4e80eec..b6093de 100644 --- a/server/routes/noteController.js +++ b/server/routes/noteController.js @@ -23,8 +23,13 @@ router.use(function setUserId (req, res, next) { // router.post('/get', function (req, res) { Note.get(userId, req.body.noteId, masterKey) - .then( data => { - res.send(data) + .then( noteObject => { + + delete noteObject.snippet_salt + delete noteObject.salt + delete noteObject.encrypted_share_password_key + + res.send(noteObject) }) }) @@ -91,8 +96,8 @@ router.post('/settrashed', function (req, res) { // // Share Note Actions // -router.post('/getshareusers', function (req, res) { - ShareNote.getUsers(userId, req.body.rawTextId) +router.post('/getshareinfo', function (req, res) { + ShareNote.getShareInfo(userId, req.body.noteId, req.body.rawTextId) .then(results => res.send(results)) }) @@ -100,7 +105,7 @@ router.post('/shareadduser', function (req, res) { // ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username, masterKey) User.getByUserName(req.body.username) .then( user => { - return ShareNote.migrateNoteToShared(userId, req.body.noteId, user.id, masterKey) + return ShareNote.addUserToSharedNote(userId, req.body.noteId, user.id, masterKey) }) .then( ({success, shareUserId}) => { @@ -110,10 +115,28 @@ router.post('/shareadduser', function (req, res) { router.post('/shareremoveuser', function (req, res) { // (userId, noteId, shareNoteUserId, shareUserId, masterKey) - ShareNote.removeUserFromShared(userId, req.body.noteId, req.body.shareUserNoteId, masterKey) + ShareNote.removeUserFromSharedNote(userId, req.body.noteId, req.body.shareUserNoteId, masterKey) .then(results => res.send(results)) }) +router.post('/enableshare', function (req, res) { + //Create Shared Encryption Key for Note + ShareNote.migrateToShared(userId, req.body.noteId, masterKey) + .then(results => res.send(true)) +}) +router.post('/getsharekey', function (req, res) { + //Get Shared Key for a note + ShareNote.decryptSharedKey(userId, req.body.noteId, masterKey) + .then(results => res.send(results)) +}) +router.post('/disableshare', function (req, res) { + //Removed shared encryption key from note + ShareNote.migrateToNormal(userId, req.body.noteId, masterKey) + .then(results => res.send(true)) +}) + + + // // Testing Action diff --git a/server/routes/publicController.js b/server/routes/publicController.js index c3e9777..65d0ddc 100644 --- a/server/routes/publicController.js +++ b/server/routes/publicController.js @@ -1,12 +1,15 @@ var express = require('express') var router = express.Router() -let Notes = require('@models/Note') +let Note = require('@models/Note') -router.post('/note', function (req, res) { - - Notes.getShared(req.body.noteId) - .then( data => res.send(data) ) +// +// Public Note action +// +router.post('/opensharednote', function (req, res) { + + Note.getShared(req.body.noteId, req.body.sharedKey) + .then(results => res.send(results)) }) diff --git a/server/routes/userController.js b/server/routes/userController.js index 05ed924..9cf717d 100644 --- a/server/routes/userController.js +++ b/server/routes/userController.js @@ -18,34 +18,31 @@ router.get('/about', function (req, res) { User.getUsername(req.headers.userId) .then( data => res.send(data) ) }) -// define the login route +// Login User router.post('/login', function (req, res) { - //Pull out variables we want - const username = req.body.username - const password = req.body.password - - let returnData = { - success: false, - token: '', - username: '' - } - - User.login(username, password) - .then( ({token, userId}) => { - - returnData['username'] = username - returnData['token'] = token - returnData['success'] = true + User.login(req.body.username, req.body.password) + .then( returnData => { res.send(returnData) - return }) .catch(e => { - console.log(e) - res.send(returnData) + res.send(false) }) }) +// Login User +router.post('/register', function (req, res) { + + User.register(req.body.username, req.body.password) + .then( returnData => { + + res.send(returnData) + }) + .catch(e => { + res.send(false) + }) +}) + // fetch counts of users notes router.post('/totals', function (req, res) {