Compare commits

..

4 Commits

Author SHA1 Message Date
Max G
9c4fff7913 * Removed arrows from notification
* Added trash can function
* Tweaked status text to always be the same
* Removed some open second note code
* Edior always focuses on text now
* Added some extra loading note messages
* Notes are now removed from search index when deleted
* Lots more things happen and update in real time on multiple machines
* Shared notes can be reverted
* WAY more tests
* Note Categories are much more reliable
* Lots of code is much cleaner
2020-05-18 07:45:35 +00:00
Max G
b0eee636b5 * Made splash page dark and updated description
* Cleaned up unused things
* Updated squire which had a comment typo update...thats it
* Background color picker has matching colors and styles to text color picker
* Added new black theme
* Moved search to main page, show it on mobile and added options to push things to notes from search with experimental tag searching
* Added active note menu buttons based on cursor location in text
* Added more instant updating if app is open in two locations for the same user Scratch Pad and home page update with new notes and new text in real time
2020-05-15 23:12:09 +00:00
Max G
2861042485 * Delete Crunch Menu Component
* Disabled Quick Note
* Note crunches over when menu is open
* Added a cool loader
* Remomoved locked notes
* Added full note encryption
* Added encrypted search index
* Added encrypted shared notes
* Made search bar have a clear and search button
* Tags only loade when clicking on the tags menu
* Tweaked home page to be a little more sane
* built out some gigantic test cases
* simplified a lot of things to make entire app easier to maintain
2020-05-10 21:15:59 +00:00
Max G
1005913c0b Fully Encrypted notes Beta
* Encrypts all notes going to the database
* Creates encrypted snippets for loading note title cards
* Creates an encrypted search index when note is changed
* Migrates users to encrypted notes on login
* Creates new encrypted master keys for newly logged in users
2020-05-06 07:10:27 +00:00
34 changed files with 2265 additions and 1256 deletions

View File

@ -10,13 +10,16 @@
<meta name="theme-color" content="#000" /> <meta name="theme-color" content="#000" />
<link rel="manifest" href="/api/static/assets/manifest.json"> <link rel="manifest" href="/api/static/assets/manifest.json">
<title>Solid Scribe - A Note Taking Website</title> <title>Solid Scribe - An easy, encrypted Note App</title>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<!-- placeholder data for scrapers with no JS --> <!-- placeholder data for scrapers with no JS -->
<style> <style>
body {
background-color: #212221;
color: #aeaeae;
}
.centered { .centered {
position: fixed; position: fixed;
top: 50%; top: 50%;
@ -37,7 +40,8 @@
<div class="centered"> <div class="centered">
<img class="logo" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo - if you can read this your connection is really slow"> <img class="logo" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo - if you can read this your connection is really slow">
<h1>Solid Scribe</h1> <h1>Solid Scribe</h1>
<h3>Loading...</h3> <h3>An easy, encrypted Note App</h3>
<h4>Loading...</h4>
</div> </div>
<div class="scrape-info"> <div class="scrape-info">

View File

@ -1,5 +1,5 @@
<template> <template>
<div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode) }"> <div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode == 2) }">
<global-site-menu /> <global-site-menu />
@ -44,8 +44,9 @@ export default {
this.$store.commit('detectIsUserOnMobile') this.$store.commit('detectIsUserOnMobile')
//Set color theme based on local storage //Set color theme based on local storage
if(localStorage.getItem('nightMode') == 'true'){ const themeNumber = localStorage.getItem('nightMode')
this.$store.commit('toggleNightMode') if(themeNumber != null){
this.$store.commit('toggleNightMode', themeNumber)
} }
//Put user data into global store on load //Put user data into global store on load

View File

@ -392,7 +392,7 @@ function createElement ( doc, tag, props, children ) {
function fixCursor ( node, root ) { function fixCursor ( node, root ) {
// In Webkit and Gecko, block level elements are collapsed and // In Webkit and Gecko, block level elements are collapsed and
// unfocussable if they have no content. To remedy this, a <BR> must be // unfocusable if they have no content. To remedy this, a <BR> must be
// inserted. In Opera and IE, we just need a textnode in order for the // inserted. In Opera and IE, we just need a textnode in order for the
// cursor to appear. // cursor to appear.
var self = root.__squire__; var self = root.__squire__;
@ -2615,6 +2615,8 @@ function Squire ( root, config ) {
this.setConfig( config ); this.setConfig( config );
root.setAttribute( 'contenteditable', 'true' ); root.setAttribute( 'contenteditable', 'true' );
// Grammarly breaks the editor, *sigh*
root.setAttribute( 'data-gramm', 'false' );
// Remove Firefox's built-in controls // Remove Firefox's built-in controls
try { try {

View File

@ -64,8 +64,8 @@
return { return {
allStyles:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, allStyles:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
colors: [ colors: [null,
"#ffebee","#ffcdd2","#ef9a9a","#e57373","#ef5350","#f44336","#e53935","#d32f2f","#c62828","#b71c1c","#fce4ec","#f8bbd0","#f48fb1","#f06292","#ec407a","#e91e63","#d81b60","#c2185b","#ad1457","#880e4f","#f3e5f5","#e1bee7","#ce93d8","#ba68c8","#ab47bc","#9c27b0","#8e24aa","#7b1fa2","#6a1b9a","#4a148c","#ede7f6","#d1c4e9","#b39ddb","#9575cd","#7e57c2","#673ab7","#5e35b1","#512da8","#4527a0","#311b92","#e8eaf6","#c5cae9","#9fa8da","#7986cb","#5c6bc0","#3f51b5","#3949ab","#303f9f","#283593","#1a237e","#e3f2fd","#bbdefb","#90caf9","#64b5f6","#42a5f5","#2196f3","#1e88e5","#1976d2","#1565c0","#0d47a1","#e1f5fe","#b3e5fc","#81d4fa","#4fc3f7","#29b6f6","#03a9f4","#039be5","#0288d1","#0277bd","#01579b","#e0f7fa","#b2ebf2","#80deea","#4dd0e1","#26c6da","#00bcd4","#00acc1","#0097a7","#00838f","#006064","#e0f2f1","#b2dfdb","#80cbc4","#4db6ac","#26a69a","#009688","#00897b","#00796b","#00695c","#004d40","#e8f5e9","#c8e6c9","#a5d6a7","#81c784","#66bb6a","#4caf50","#43a047","#388e3c","#2e7d32","#1b5e20","#f1f8e9","#dcedc8","#c5e1a5","#aed581","#9ccc65","#8bc34a","#7cb342","#689f38","#558b2f","#33691e","#f9fbe7","#f0f4c3","#e6ee9c","#dce775","#d4e157","#cddc39","#c0ca33","#afb42b","#9e9d24","#827717","#fffde7","#fff9c4","#fff59d","#fff176","#ffee58","#ffeb3b","#fdd835","#fbc02d","#f9a825","#f57f17","#fff8e1","#ffecb3","#ffe082","#ffd54f","#ffca28","#ffc107","#ffb300","#ffa000","#ff8f00","#ff6f00","#fff3e0","#ffe0b2","#ffcc80","#ffb74d","#ffa726","#ff9800","#fb8c00","#f57c00","#ef6c00","#e65100","#fbe9e7","#ffccbc","#ffab91","#ff8a65","#ff7043","#ff5722","#f4511e","#e64a19","#d84315","#bf360c","#efebe9","#d7ccc8","#bcaaa4","#a1887f","#8d6e63","#795548","#6d4c41","#5d4037","#4e342e","#3e2723","#fafafa","#f5f5f5","#eeeeee","#e0e0e0","#bdbdbd","#9e9e9e","#757575","#616161","#424242","#212121","#eceff1","#cfd8dc","#b0bec5","#90a4ae","#78909c","#607d8b","#546e7a","#455a64","#37474f","#263238","#ffffff","#000000"], 'rgb(67,67,67)','rgb(102,102,102)','rgb(153,153,153)','rgb(183,183,183)','rgb(204,204,204)','rgb(217,217,217)','rgb(239,239,239)','rgb(243,243,243)','rgb(255,255,255)','rgb(152,0,0)','rgb(255,0,0)','rgb(255,153,0)','rgb(255,255,0)','rgb(0,255,0)','rgb(0,255,255)','rgb(74,134,232)','rgb(0,0,255)','rgb(153,0,255)','rgb(255,0,255)','rgb(230,184,175)','rgb(244,204,204)','rgb(252,229,205)','rgb(255,242,204)','rgb(217,234,211)','rgb(208,224,227)','rgb(201,218,248)','rgb(207,226,243)','rgb(217,210,233)','rgb(234,209,220)','rgb(221,126,107)','rgb(234,153,153)','rgb(249,203,156)','rgb(255,229,153)','rgb(182,215,168)','rgb(162,196,201)','rgb(164,194,244)','rgb(159,197,232)','rgb(180,167,214)','rgb(213,166,189)','rgb(204,65,37)','rgb(224,102,102)','rgb(246,178,107)','rgb(255,217,102)','rgb(147,196,125)','rgb(118,165,175)','rgb(109,158,235)','rgb(111,168,220)','rgb(142,124,195)','rgb(194,123,160)','rgb(166,28,0)','rgb(204,0,0)','rgb(230,145,56)','rgb(241,194,50)','rgb(106,168,79)','rgb(69,129,142)','rgb(60,120,216)','rgb(61,133,198)','rgb(103,78,167)','rgb(166,77,121)','rgb(133,32,12)','rgb(153,0,0)','rgb(180,95,6)','rgb(191,144,0)','rgb(56,118,29)','rgb(19,79,92)','rgb(17,85,204)','rgb(11,83,148)','rgb(53,28,117)','rgb(116,27,71)','rgb(91,15,0)','rgb(102,0,0)','rgb(120,63,4)','rgb(127,96,0)','rgb(39,78,19)','rgb(12,52,61)','rgb(28,69,135)','rgb(7,55,99)','rgb(32,18,77)','rgb(76,17,48)'],
icons: ['ambulance','anchor','balance scale','bath','bed','beer','bell','bell slash','bell slash outline','bicycle','binoculars','birthday cake','blind','bomb','book','bookmark','briefcase','building','car','coffee','crosshairs','dollar sign','eye','eye slash','fighter jet','fire','fire extinguisher','flag','flag checkered','flask','gamepad','gavel','gift','glass martini','globe','graduation cap','h square','heart','heart outline','heartbeat','home','hospital','hospital outline','image','image outline','images','images outline','industry','info','info circle','key','leaf','lemon','lemon outline','life ring','life ring outline','lightbulb','lightbulb outline','location arrow','low vision','magnet','male','map','map outline','map marker','map marker alternate','map pin','map signs','medkit','money bill alternate','money bill alternate outline','motorcycle','music','newspaper','newspaper outline','paw','phone','phone square','phone volume','plane','plug','plus','plus square','plus square outline','print','recycle','road','rocket','search','search minus','search plus','ship','shopping bag','shopping basket','shopping cart','shower','street view','subway','suitcase','tag','tags','taxi','thumbtack','ticket alternate','tint','train','tree','trophy','truck','tty','umbrella','university','utensil spoon','utensils','wheelchair','wifi','wrench'] icons: ['ambulance','anchor','balance scale','bath','bed','beer','bell','bell slash','bell slash outline','bicycle','binoculars','birthday cake','blind','bomb','book','bookmark','briefcase','building','car','coffee','crosshairs','dollar sign','eye','eye slash','fighter jet','fire','fire extinguisher','flag','flag checkered','flask','gamepad','gavel','gift','glass martini','globe','graduation cap','h square','heart','heart outline','heartbeat','home','hospital','hospital outline','image','image outline','images','images outline','industry','info','info circle','key','leaf','lemon','lemon outline','life ring','life ring outline','lightbulb','lightbulb outline','location arrow','low vision','magnet','male','map','map outline','map marker','map marker alternate','map pin','map signs','medkit','money bill alternate','money bill alternate outline','motorcycle','music','newspaper','newspaper outline','paw','phone','phone square','phone volume','plane','plug','plus','plus square','plus square outline','print','recycle','road','rocket','search','search minus','search plus','ship','shopping bag','shopping basket','shopping cart','shower','street view','subway','suitcase','tag','tags','taxi','thumbtack','ticket alternate','tint','train','tree','trophy','truck','tty','umbrella','university','utensil spoon','utensils','wheelchair','wifi','wrench']
} }
}, },
@ -84,15 +84,12 @@
this.colors.forEach((color,i) => { this.colors.forEach((color,i) => {
if(i%20 <= 10){
return
}
let mod = (i % 10)+1 //1 - 10 let mod = (i % 10)+1 //1 - 10
let lines = [3, 5, 8, 9, 10] let lines = [3, 5, 8, 9, 10]
if(lines.includes(mod)){ // if(lines.includes(mod)){
reduced.push(color) reduced.push(color)
} // }
}) })
reduced.push("#000") reduced.push("#000")
@ -110,6 +107,11 @@
//Set not background to color that was chosen //Set not background to color that was chosen
this.allStyles.noteBackground = inColor this.allStyles.noteBackground = inColor
if(inColor == null){
this.$emit('changeColor', this.allStyles)
return
}
//Automatically select note text color //Automatically select note text color
// Convert hex color to RGB - http://gist.github.com/983661 // Convert hex color to RGB - http://gist.github.com/983661
@ -148,16 +150,18 @@
<style type="text/css" scoped> <style type="text/css" scoped>
.icon-button { .icon-button {
height: 40px; height: 40px;
width: 14.2%; width: calc(10% - 7px);
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
font-size: 1.3em; font-size: 1.3em;
} }
.color-button { .color-button {
height: 50px; display: inline-block;
width: 20%; width: calc(10% - 7px);
display: block; height: 30px;
border-radius: 30px;
box-shadow: 0px 1px 3px 0px #3e3e3e;
margin: 7px 7px 0 0;
cursor: pointer; cursor: pointer;
float: left;
} }
</style> </style>

View File

@ -1,43 +0,0 @@
<template>
<div>
<p>Crunch Menu</p>
<div v-for="(item, index) in items">
<slot :name="index"></slot>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'CrunchMenu',
data () {
return {
items: []
}
},
beforeMount(){
},
mounted(){
console.log(this)
// console.log(this.$slots.default)
this.$slots.default.forEach( vnode => {
if(vnode.tag && vnode.tag.length > 0){
this.items.push(vnode)
}
})
console.log(this.items)
},
methods: {
onClickTag(index){
console.log('yup')
},
}
}
</script>
<style type="text/css" scoped>
</style>

View File

@ -70,9 +70,7 @@
<template> <template>
<div class="popup-body slide-in-bottom" v-on:click="dismiss" v-if="notifications.length > 0"> <div class="popup-body slide-in-bottom" v-on:click="dismiss" v-if="notifications.length > 0">
<div class="popup-row color-fade" v-for="item in notifications"> <div class="popup-row color-fade" v-for="item in notifications">
<i class="disabled angle left icon"></i>
<span>{{ item }}</span> <span>{{ item }}</span>
<i class="disabled angle right icon"></i>
</div> </div>
</div> </div>
</template> </template>

View File

@ -125,7 +125,6 @@
<i class="open folder outline icon"></i> <i class="open folder outline icon"></i>
</router-link> </router-link>
</div> </div>
<div class="two wide center aligned bottom aligned column"> <div class="two wide center aligned bottom aligned column">
@ -138,7 +137,6 @@
<i class="green moon outline icon"></i> <i class="green moon outline icon"></i>
</div> </div>
<search-input v-if="loggedIn && mobile"></search-input>
<!-- mobile create note button --> <!-- mobile create note button -->
<span v-if="loggedIn"> <span v-if="loggedIn">
<span v-if="!disableNewNote" @click="createNote" class="ui large green compact icon button"> <span v-if="!disableNewNote" @click="createNote" class="ui large green compact icon button">
@ -201,7 +199,7 @@
<div class="menu-section" v-if="loggedIn"> <div class="menu-section" v-if="loggedIn">
<router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick"> <router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick">
<i class="paper plane outline icon"></i>Quick Note <i class="sticky note outline icon"></i>Scratch Pad
</router-link> </router-link>
</div> </div>
@ -217,10 +215,12 @@
<div class="menu-section"> <div class="menu-section">
<div v-on:click="toggleNightMode" class="menu-item menu-button"> <div v-on:click="toggleNightMode" class="menu-item menu-button">
<span v-if="$store.getters.getIsNightMode"> <span v-if="$store.getters.getIsNightMode == 0">
<i class="moon outline icon"></i>Black Theme</span>
<span v-if="$store.getters.getIsNightMode == 1">
<i class="moon outline icon"></i>Night Theme</span>
<span v-if="$store.getters.getIsNightMode == 2">
<i class="moon outline icon"></i>Light Theme</span> <i class="moon outline icon"></i>Light Theme</span>
<span v-else>
<i class="moon outline icon"></i>Dark Theme</span>
</div> </div>
</div> </div>
@ -257,7 +257,7 @@
}, },
data: function(){ data: function(){
return { return {
version: '1.0.5', version: '2.2.2',
username: '', username: '',
collapsed: false, collapsed: false,
mobile: false, mobile: false,
@ -329,7 +329,7 @@
.then(response => { .then(response => {
if(response.data && response.data.id){ if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id) //Redirect to note page if user is not on it
this.$bus.$emit('open_note', response.data.id) this.$bus.$emit('open_note', response.data.id)
this.disableNewNote = false this.disableNewNote = false
} }
@ -380,7 +380,7 @@
location.reload(true) location.reload(true)
}, },
getVersionIcon(){ getVersionIcon(){
const icons = ['cat','crow','dog','dove','dragon','fish','frog','hippo','horse','kiwi bird','otter','spider'] const icons = ['cat','crow','dog','dove','dragon','fish','frog','hippo','horse','kiwi bird','otter','spider', 'smile', 'robot', 'hat wizard', 'microchip', 'atom', 'grin tongue squint', 'radiation']
const index = ( parseInt(this.version.replace(/\./g,'')) % (icons.length)) const index = ( parseInt(this.version.replace(/\./g,'')) % (icons.length))
return icons[index] return icons[index]

View File

@ -0,0 +1,67 @@
<template>
<div class="loading-container">
<svg version="1.1" id="L6" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
<rect fill="none" :stroke="$store.getters.getIsNightMode > 0 ? '#FFF':'#16ab39'" stroke-width="4" x="25" y="25" width="50" height="50" rx="5">
<animateTransform
attributeName="transform"
dur="0.5s"
from="0 50 50"
to="180 50 50"
type="rotate"
id="strokeBox"
attributeType="XML"
begin="rectBox.end"/>
</rect>
<rect x="25" y="25" :fill="$store.getters.getIsNightMode > 0 ? '#FFF':'#16ab39'" width="50" height="50">
<animate
attributeName="height"
dur="1.3s"
attributeType="XML"
from="50"
to="0"
id="rectBox"
fill="freeze"
begin="0s;strokeBox.end"/>
</rect>
</svg>
<div class="loading-message" v-if="message">{{ message }}</div>
</div>
</template>
<script>
export default {
name: 'LoadingIcon',
props:[ 'message' ],
data () {
return {
items: []
}
},
beforeMount(){
},
mounted(){
},
methods: {
onClickTag(index){
console.log('yup')
},
}
}
</script>
<style type="text/css" scoped>
.loading-container {
text-align: center;
width: 100%;
height: 100px;
margin: 20px 0;
}
.loading-container svg {
width: 60px;
height: 60px;
}
.loading-message {
font-size: 1.5em;
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<span> <span>
<span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete" data-inverted="" data-position="top right"> <span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete Forever" data-inverted="" data-position="top right">
<i class="trash alternate icon"></i> <i class="trash alternate icon"></i>
</span> </span>
<span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="top right" data-inverted=""> <span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="top right" data-inverted="">

View File

@ -2,9 +2,9 @@
<!-- change class to .master-note-edit to have it popup on the screen --> <!-- change class to .master-note-edit to have it popup on the screen -->
<div <div
id="InputNotes" id="InputNotes"
class="master-note-edit" class="master-note-edit full-focus"
@keyup.esc="close()" @keyup.esc="close()"
:class="[{ 'full-focus':(fullFocusEditor) }, 'position-'+position ]" :class="[ 'position-'+position ]"
> >
<!-- Main Menu --> <!-- Main Menu -->
@ -21,25 +21,25 @@
<div class="edit-divide"></div> <div class="edit-divide"></div>
<div class="edit-button" v-on:click="toggleList('ul')" data-tooltip="Task List" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="toggleList('ul')" data-tooltip="Task List" data-position="bottom center" data-inverted :class="{'edit-active':activeToDo}">
<i class="tasks icon"></i> <i class="tasks icon"></i>
</div> </div>
<div class="edit-button" v-on:click="toggleList('ol')" data-tooltip="Numbered List" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="toggleList('ol')" data-tooltip="Numbered List" data-position="bottom center" data-inverted :class="{'edit-active':activeList}">
<i class="list ol icon"></i> <i class="list ol icon"></i>
</div> </div>
<div class="edit-button" v-on:click="colorpicker = true" data-tooltip="Text Color" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="colorpicker = true" data-tooltip="Text Color" data-position="bottom center" data-inverted :style="{'color':activeColor}">
<i class="palette icon"></i> <i class="palette icon"></i>
</div> </div>
<div class="edit-button" v-on:click="toggleBold()" data-tooltip="Bold" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="toggleBold()" data-tooltip="Bold" data-position="bottom center" data-inverted :class="{'edit-active':activeBold}">
<i class="bold icon"></i> <i class="bold icon"></i>
</div> </div>
<div class="edit-button" v-on:click="toggleItalic()" data-tooltip="Quote" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="toggleItalic()" data-tooltip="Quote" data-position="bottom center" data-inverted :class="{'edit-active':activeQuote}">
<i class="quote left icon"></i> <i class="quote left icon"></i>
</div> </div>
<div class="edit-button" v-on:click="modifyFont('1.4em')" data-tooltip="Title" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="modifyFont('1.4em')" data-tooltip="Title" data-position="bottom center" data-inverted :class="{'edit-active':activeTitle}">
<i class="text height icon"></i> <i class="text height icon"></i>
</div> </div>
<div class="edit-button" v-on:click="editor.increaseQuoteLevel()" data-tooltip="Indent" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="editor.increaseQuoteLevel()" data-tooltip="Indent" data-position="bottom center" data-inverted>
@ -63,7 +63,7 @@
<div class="edit-divide"></div> <div class="edit-divide"></div>
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/colors`)" data-tooltip="Note Color" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/colors`)" data-tooltip="Note Color" data-position="bottom center" data-inverted :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<i class="paint brush icon"></i> <i class="paint brush icon"></i>
</div> </div>
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)" data-tooltip="Tags" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)" data-tooltip="Tags" data-position="bottom center" data-inverted>
@ -86,17 +86,6 @@
<div class="edit-divide"></div> <div class="edit-divide"></div>
<!-- protect -->
<div class="edit-button" v-if="!isEncrypted"
v-on:click="$router.push(`/notes/open/${noteid}/menu/passwordprotect`)" data-tooltip="Password Protect" data-position="bottom center" data-inverted>
<i class="shield alternate icon"></i>
</div>
<!-- data-tooltip="Remove Password Protection" -->
<div class="edit-button" v-if="isEncrypted && isDecrypted" v-on:click="disableEncryption()" data-tooltip="Close" data-position="bottom center" data-inverted>
<i class="unlock icon"></i>
</div>
<div class="edit-button" v-on:click="onToggleArchived()" :data-tooltip="archived == 1?'Move to main list':'Move to Archive'" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="onToggleArchived()" :data-tooltip="archived == 1?'Move to main list':'Move to Archive'" data-position="bottom center" data-inverted>
<span v-if="archived == 1"><i class="green archive icon"></i></span> <span v-if="archived == 1"><i class="green archive icon"></i></span>
<span v-if="archived != 1"><i class="archive icon"></i></span> <span v-if="archived != 1"><i class="archive icon"></i></span>
@ -112,6 +101,8 @@
<i class="folder icon"></i> <i class="folder icon"></i>
</div> </div>
<span>{{ statusText }}</span>
</div> </div>
<!-- <span :data-tooltip="`Created: ${$helpers.timeAgo(created)}`"> <!-- <span :data-tooltip="`Created: ${$helpers.timeAgo(created)}`">
@ -121,18 +112,20 @@
<div class="bottom-edit-menu"></div> <div class="bottom-edit-menu"></div>
<div class="input-container-wrapper" :class="{ 'size-down':(sizeDown == true)}" > <div class="input-container-wrapper" :class="{ 'side-menu-open':sideMenuOpen, 'size-down':(sizeDown == true),}" :style="{ 'background-color':styleObject['noteBackground'] }">
<!-- Loading indicator -->
<div v-if="loading" class="loading-note">
<div class="ui active dimmer">
<div class="ui text loader">{{loadingMessage}}</div>
</div>
</div>
<!-- Squire box grows --> <!-- Squire box grows -->
<div class="note-wrapper" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}"> <div class="note-wrapper" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<!-- Loading indicator -->
<transition name="fade">
<div v-if="loading || forceShowLoading" class="loading-note" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<div class="loading-text">
<loading-icon :message="loadingMessage" />
</div>
</div>
</transition>
<!-- Title input area --> <!-- Title input area -->
<textarea <textarea
ref="titleTextarea" ref="titleTextarea"
@ -144,53 +137,11 @@
v-on:blur="save" type="text" v-model="noteTitle" placeholder="Title" class="stealth-input"> v-on:blur="save" type="text" v-model="noteTitle" placeholder="Title" class="stealth-input">
</textarea> </textarea>
<!-- Squire Box - only appears if decrypted --> <!-- Squire Box -->
<div v-show="isDecrypted" id="squire-id" class="squire-box" ref="squirebox" placeholder="Note Text"></div> <div id="squire-id" class="squire-box" ref="squirebox" placeholder="Note Text"></div>
<!-- Decrypt note prompt -->
<div v-if="isEncrypted && !isDecrypted" class="ui basic padded segment">
<div class="ui raised segment">
<h3 class="ui center aligned icon header">
<i class="green lock alternate icon"></i>
<span v-if="!lockedOut">
This note is encrypted and requires a password to be opened.
</span>
<!-- note is locked for 5 minutes -->
<span v-if="lockedOut">
To many unlock attempts. Note is locked for 5 minutes.
</span>
</h3>
<!-- Decrypt note -->
<div class="ui form" v-if="!lockedOut">
<h5 class="ui horizontal divider header" v-if="passwordHint && passwordHint.length > 0">
Hint: {{ passwordHint }}
</h5>
<div class="field">
<input :name="`randomThing-${noteid}`" :id="`yupper-${noteid}`"type="password" v-model="password" placeholder="Note Password" v-on:keyup.enter="decryptNote" autofocus ref="decryptNotePrompt">
</div>
<div class="field">
<div v-on:click="decryptNote" class="ui green fluid button" v-if="password.length >= 3">
Unlock Note
</div>
<div class="ui disabled fluid button" v-if="password.length < 3">
Unlock Note
</div>
</div>
</div>
</div>
</div>
</div> </div>
<!-- && this.$store.getters.getIsUserOnMobile -->
<span class="note-status-indicator" v-on:click="save()" v-if="statusText != 'Saved' && $store.getters.getIsUserOnMobile">
<div class="ui green button">{{statusText}}</div>
</span>
</div> </div>
<!-- color picker --> <!-- color picker -->
@ -267,55 +218,6 @@
</div> </div>
</side-slide-menu> </side-slide-menu>
<side-slide-menu v-show="passwordprotect" v-on:close="passwordprotect = false" :fullShadow="true" name="encrypt note">
<div class="ui basic segment" v-if="isDecrypted && isEncrypted">
<p>Note Decrypted</p>
<div class="ui green button" v-on:click="lockNote">Lock Note</div>
</div>
<div v-if="!isEncrypted" class="ui basic segment">
<div class="ui top attached segment">
<h2><i class="green lock alternate icon"></i>Password protect this Note</h2>
<p>Password protection will prevent anyone from reading the text of this note, unless they enter the correct password.</p>
<p><b>Only the note text is protected. Title, tags, and files are not encrypted and remain visible without a password.</b></p>
<p>The password you select will only be used for this note. You can use the same password on multiple notes. The note will be encrypted using the password entered. A longer password will be more secure.</p>
<h4><i class="red icon exclamation triangle"></i> Warning. There is no way to recover a lost password.</h4>
</div>
<div class="ui bottom attached segment">
<div class="ui form">
<div class="field">
<label>New Password to lock this note</label>
<input :name="`randomThing-${noteid}`" :id="`yupper-${noteid}`"type="password" v-model="password" placeholder="Note Password">
</div>
<div class="field" v-if="password.length > 3">
<label>Confirm Password</label>
<input :name="`randomStuff-${noteid}`" :id="`heckye-${noteid}`"type="password" v-model="passwordConfirm" placeholder="Confirm Password">
</div>
<div class="field" v-if="password.length > 3">
<label>Password Hint - visible when unlocking note</label>
<input :name="`randomStuzz-${noteid}`" :id="`heckyo-${noteid}`"type="text" v-model="passwordHint" placeholder="Optional Password Hint" v-on:keyup.enter="enableEncryption">
</div>
<div class="field" v-if="passwordConfirm.length > 3 && password != passwordConfirm">
<div v-on:click="enableEncryption" class="ui disabled green button">
Passwords do not match
</div>
</div>
<div class="field" v-if="passwordConfirm.length > 3 && password == passwordConfirm">
<div v-on:click="enableEncryption" class="ui green button">
Protect!
</div>
</div>
</div>
</div>
</div>
</side-slide-menu>
<!-- Show side shades if user is on desktop only --> <!-- Show side shades if user is on desktop only -->
<div class="full-focus-shade shade1" <div class="full-focus-shade shade1"
:class="{ 'slide-out-left':(sizeDown == true) }" :class="{ 'slide-out-left':(sizeDown == true) }"
@ -346,11 +248,13 @@
'share-note-component': () => import('@/components/ShareNoteComponent.vue'), 'share-note-component': () => import('@/components/ShareNoteComponent.vue'),
'color-tooltip':require('@/components/TextColorTooltipComponent.vue').default, 'color-tooltip':require('@/components/TextColorTooltipComponent.vue').default,
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default 'nm-button':require('@/components/NoteMenuButtonComponent.vue').default,
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
}, },
data(){ data(){
return { return {
loading: true, loading: true,
forceShowLoading: true,
loadingMessage: 'Loading Note', loadingMessage: 'Loading Note',
currentNoteId: 0, currentNoteId: 0,
modified: false, modified: false,
@ -361,7 +265,7 @@
updated: '', updated: '',
shareUsername: null, shareUsername: null,
diffNoteText: '', diffNoteText: '',
statusText: 'Saved', statusText: 'Saved.',
lastNoteHash: null, lastNoteHash: null,
saveDebounce: null, //Prevent save from being called numerous times quickly saveDebounce: null, //Prevent save from being called numerous times quickly
updated: 'Never', updated: 'Never',
@ -375,12 +279,7 @@
sizeDown: false, //Used to animate close state sizeDown: false, //Used to animate close state
colorPickerLocation: null,
fullFocusEditor: true, //Initialized editor instance
//Settings vars //Settings vars
showAllSettings: true,
lastVisibilityState: null, lastVisibilityState: null,
//All the squire settings //All the squire settings
@ -388,38 +287,35 @@
// pastFocusedNode: null, // pastFocusedNode: null,
usersOnNote: 0, usersOnNote: 0,
sideMenuOpen: false,
tags: false, tags: false,
colors: false, colors: false,
images: false, images: false,
options: false, options: false,
colorpicker: false, colorpicker: false,
//Encryption options //active button states
passwordHint: '', activeBold: false,
password: '', //Field Variables, only for form activeQuote: false,
passwordConfirm: '', //Only a form variable activeTitle: false,
hashedPass: '', //sha-256 password hash, sends to server for decryption activeList: false,
isEncrypted: false, activeToDo: false,
isDecrypted: false, activeColor: null,
passwordprotect: false,
decryptAttempts: 0,
lockedOut: false,
autoLockTimeout: null,
} }
}, },
watch: { watch: {
noteid:function(newVal, oldVal){ noteid:function(newVal, oldVal){
if(newVal == this.currentNoteId){ // if(newVal == this.currentNoteId){
return // return
} // }
if(newVal == oldVal){ // if(newVal == oldVal){
return // return
} // }
this.currentNoteId = newVal // this.currentNoteId = newVal
this.loadNote(this.currentNoteId) // this.loadNote(this.currentNoteId)
}, },
urlData(newVal, oldVal){ urlData(newVal, oldVal){
@ -431,9 +327,9 @@
} }
//Reset all note menus on URL change //Reset all note menus on URL change
this.sideMenuOpen = false
this.colors = false this.colors = false
this.tags = false this.tags = false
this.passwordprotect = false
this.options = false this.options = false
this.images = false this.images = false
@ -441,7 +337,7 @@
if(newVal.openMenu){ if(newVal.openMenu){
//Only modify menu boolean if its defined //Only modify menu boolean if its defined
if(typeof this[newVal.openMenu] == 'boolean'){ if(typeof this[newVal.openMenu] == 'boolean'){
this.sideMenuOpen = true
this[newVal.openMenu] = true this[newVal.openMenu] = true
} }
} }
@ -458,22 +354,23 @@
}, },
beforeDestroy(){ beforeDestroy(){
this.password = ''
this.passwordConfirm = ''
this.hashedPass = ''
clearTimeout(this.autoLockTimeout)
// this.$io.emit('leave_room', this.rawTextId) // this.$io.emit('leave_room', this.rawTextId)
this.$bus.$off('new_file_upload') this.$bus.$off('new_file_upload')
document.removeEventListener('visibilitychange', this.checkForUpdatedNote) document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
this.editor.destroy() if(this.editor){
this.editor.destroy()
}
}, },
mounted: function() { mounted: function() {
setTimeout(()=>{
this.forceShowLoading = false
}, 500)
document.addEventListener('visibilitychange', this.checkForUpdatedNote) document.addEventListener('visibilitychange', this.checkForUpdatedNote)
this.$nextTick(() => { this.$nextTick(() => {
@ -488,18 +385,49 @@
this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' }) this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' })
this.setText(this.noteText) this.setText(this.noteText)
//focus on open, not on mobile, thats annoying this.lastNoteHash = this.hashString(this.getText())
if(!this.$store.getters.getIsUserOnMobile){ // console.log('hash on load', this.lastNoteHash)
// this.editor.focus()
if(this.noteTitle.length == 0){ //focus on open, not on mobile, it causes the keyboard to pop up, thats annoying
this.$refs.titleTextarea.focus() if(!this.$store.getters.getIsUserOnMobile){
} else { this.editor.focus()
this.editor.focus() this.editor.moveCursorToEnd()
this.editor.moveCursorToEnd() }
//Change button states on editor when element is active
//eg; Bold button turns green when on bold text
this.editor.addEventListener('pathChange', e => {
//Reset all button states
this.activeBold = false
this.activeTitle = false
this.activeQuote = false
this.activeList = false
this.activeToDo = false
this.activeColor = null
let colors = e.path.match(/\d+/g)
if(colors && colors.length == 3){
this.activeColor=`rgb(${colors.join(',')})`
} }
} //@ TODO - Update this to match all elements, like color and bold
// index of and then the specific thing might more indexOf('B'), indexOf('I'), etc
let element = e.path.split('>').pop()
switch (element) {
case 'B': this.activeBold = true; break;
case 'I': this.activeQuote = true; break;
case 'SPAN.size[fontSize=1.4em]': this.activeTitle = true; break;
}
let parent = e.path.split('>').shift()
switch (parent) {
case 'OL': this.activeList = true; break;
case 'UL': this.activeToDo = true; break;
}
})
//Click Event - Open links when clicked in editor or toggle checks //Click Event - Open links when clicked in editor or toggle checks
this.editor.addEventListener('click', e => { this.editor.addEventListener('click', e => {
@ -877,20 +805,23 @@
loadNote(noteId){ loadNote(noteId){
//Generate a random loading message //Generate a random loading message
let mod = ['Gently','Calmly','Lovingly','Quickly','','','','','','','','','','','','','']
let doing = ['Loading','Loading','Getting','Fetching','Grabbing','Sequencing','Organizing','Untangling','Processing','Refining','Extracting','Fusing','Pruning','Expanding','Enlarging','Transfiguring','Quantizing','Ingratiating','Lumping'] let doing = ['Loading','Loading','Getting','Fetching','Grabbing','Sequencing','Organizing','Untangling','Processing','Refining','Extracting','Fusing','Pruning','Expanding','Enlarging','Transfiguring','Quantizing','Ingratiating','Lumping']
let thing = ['Note','Note','Note','Note','Data','Text','Document','Algorithm','Buffer','Client','Download','File','Frame','Graphics','Hardware','HTML','Interface','Logic','Mainframe','Memory','Media','Nodes','Network','Chaos'] let thing = ['Note','Note','Note','Note','Data','Text','Document','Algorithm','Buffer','Client','Download','File','Frame','Graphics','Hardware','HTML','Interface','Logic','Mainframe','Memory','Media','Nodes','Network','Chaos']
let p1 = doing[Math.floor(Math.random() * doing.length)]
let p2 = thing[Math.floor(Math.random() * thing.length)] let p1 = mod[Math.floor(Math.random() * mod.length)]
this.loadingMessage = p1 + ' ' + p2 let p2 = doing[Math.floor(Math.random() * doing.length)]
let p3 = thing[Math.floor(Math.random() * thing.length)]
this.loadingMessage = `${p1} ${p2} ${p3}`
//Component is activated with NoteId in place, lookup text with associated ID //Component is activated with NoteId in place, lookup text with associated ID
if(this.$store.getters.getLoggedIn){ if(this.$store.getters.getLoggedIn){
axios.post('/api/note/get', { 'noteId': this.noteid, 'password':this.hashedPass }) axios.post('/api/note/get', { 'noteId': this.noteid })
.then(response => { .then(response => {
//Block notes you don't have access to from opening //Block notes you don't have access to from opening
if(response.data === false){ if(response.data === false){
this.$bus.$emit('notification', 'Invalid Note') this.$bus.$emit('notification', 'Error opening Note')
this.close(true) this.close(true)
return return
} }
@ -899,7 +830,6 @@
this.currentNoteId = this.noteid this.currentNoteId = this.noteid
this.rawTextId = response.data.rawTextId this.rawTextId = response.data.rawTextId
this.shareUsername = response.data.shareUsername this.shareUsername = response.data.shareUsername
this.passwordHint = response.data.password_hint
this.created = response.data.created this.created = response.data.created
this.updated = response.data.updated this.updated = response.data.updated
@ -911,7 +841,6 @@
this.noteText = response.data.text this.noteText = response.data.text
this.diffNoteText = response.data.text this.diffNoteText = response.data.text
this.lastNoteHash = this.hashString(response.data.text)
//Set up note colors //Set up note colors
if(response.data.color){ if(response.data.color){
this.styleObject = JSON.parse(response.data.color) this.styleObject = JSON.parse(response.data.color)
@ -925,29 +854,12 @@
this.loading = false this.loading = false
this.isDecrypted = response.data.decrypted
this.isEncrypted = response.data.encrypted == 1
this.decryptAttempts = response.data.decrypt_attempts_count
this.lockedOut = response.data.lockedOut
//If password is required, display a prompt and focus on it
if(this.password.length == 0 && this.isEncrypted && !this.isDecrypted){
this.$nextTick(() => {
if(this.$refs.decryptNotePrompt){
// this.editor.moveCursorToEnd()
this.$refs.decryptNotePrompt.focus()
}
})
}
this.$nextTick(() => { this.$nextTick(() => {
//Adjust note title size after load //Adjust note title size after load
this.titleResize() this.titleResize()
this.setupWebSockets() this.setupWebSockets()
this.initSquire() this.initSquire()
this.startAutolockTimer()
}) })
}) })
@ -1123,19 +1035,19 @@
}, },
onKeyup(){ onKeyup(){
this.statusText = 'Save' this.statusText = 'Modded'
this.diffText() // this.diffText()
//Each note, save after 5 seconds, focus lost or 30 characters typed. //Each note, save after 15 seconds, focus lost or 30 characters typed.
clearTimeout(this.editDebounce) clearTimeout(this.editDebounce)
this.editDebounce = setTimeout(() => { this.editDebounce = setTimeout(() => {
this.save() this.save()
}, 5000) }, 15 * 1000)
//Save after 30 keystrokes //Save after 50 keystrokes
this.keyPressesCounter = (this.keyPressesCounter + 1) this.keyPressesCounter = (this.keyPressesCounter + 1)
if(this.keyPressesCounter > 30){ if(this.keyPressesCounter > 50){
this.keyPressesCounter = 0 this.keyPressesCounter = 0
this.save() this.save()
} }
@ -1147,23 +1059,16 @@
// return resolve(true) // return resolve(true)
//Encrypted notes that are not decrypted should not be saved
if(this.isEncrypted && !this.isDecrypted){
return resolve(true)
}
//Don't save note if its hash doesn't change //Don't save note if its hash doesn't change
const currentNoteText = this.getText() const currentNoteText = this.getText()
if( this.lastNoteHash == this.hashString( currentNoteText )){ const currentHash = this.hashString( currentNoteText )
this.statusText = 'Saved' if( this.lastNoteHash == currentHash){
this.statusText = 'Saved.'
return resolve(true) return resolve(true)
} }
//If user accidentally clears note, it won't delete it //If user accidentally clears note, it won't delete it
if(currentNoteText == ''){ if(currentNoteText == ''){
this.statusText = 'Empty'
console.log('Prevented from saving empty note.')
return resolve(true) return resolve(true)
} }
@ -1174,19 +1079,20 @@
'color': JSON.stringify(this.styleObject), //Save little json color object 'color': JSON.stringify(this.styleObject), //Save little json color object
'pinned': this.pinned, 'pinned': this.pinned,
'archived': this.archived, 'archived': this.archived,
'password': this.hashedPass, 'hash': currentHash,
'hint': this.passwordHint,
} }
// console.log('Save Hash', currentHash)
this.statusText = 'Saving' this.statusText = 'Saving'
axios.post('/api/note/update', postData).then( response => { axios.post('/api/note/update', postData).then( response => {
this.statusText = 'Saved' this.statusText = 'Saved.'
this.updated = Math.round((+new Date)/1000) this.updated = Math.round((+new Date)/1000)
this.modified = true this.modified = true
//Update last saved note hash //Update last saved note hash
this.lastNoteHash = this.hashString( currentNoteText ) // this.lastNoteHash = this.hashString( currentNoteText )
this.startAutolockTimer() this.lastNoteHash = currentHash
return resolve(true) return resolve(true)
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Save Note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Save Note') })
@ -1194,7 +1100,9 @@
}, },
checkForUpdatedNote(){ checkForUpdatedNote(){
// return //Ignore visibility changes, handle this with socket IO
//Just keep it always up to date if user is on note
return
//If user leaves page then returns to page, reload the first batch //If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){ if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
@ -1206,6 +1114,10 @@
updated: this.updated updated: this.updated
} }
console.log('Focus regained with note open.')
console.log('Attempting to fix diff text. fix this. Search spleen')
return
axios.post('/api/note/difftext', postData) axios.post('/api/note/difftext', postData)
.then( ({data}) => { .then( ({data}) => {
@ -1224,18 +1136,15 @@
//Track visibility state //Track visibility state
this.lastVisibilityState = document.visibilityState this.lastVisibilityState = document.visibilityState
}, },
hashString(text){ hashString(inText){
text = this.noteTitle + text let text = this.noteTitle + inText
var hash = 0; let hash = 0;
if (text == null || text.length == 0) { if (text == null || text.length == 0) {
return hash; return hash;
} }
//Simplified for speed
// return text.length
for (let i = 0; i < text.length; i++) { for (let i = 0; i < text.length; i++) {
let char = text.charCodeAt(i); let char = text.charCodeAt(i);
hash = ((hash<<5)-hash)+char; hash = ((hash<<5)-hash)+char;
@ -1255,6 +1164,11 @@
this.save().then( result => { this.save().then( result => {
//If note was modified, trigger reindex on close
if(this.modified){
axios.post('/api/note/reindex')
}
this.sizeDown = true this.sizeDown = true
//This timeout allows animation to play before closing //This timeout allows animation to play before closing
setTimeout(() => { setTimeout(() => {
@ -1267,6 +1181,11 @@
}, },
setupWebSockets(){ setupWebSockets(){
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
// console.log('Current hash', this.lastNoteHash)
// console.log('Incoming Hash', hash)
})
return return
//Tell server to push this note into a room //Tell server to push this note into a room
@ -1281,62 +1200,6 @@
this.patchText(incomingDiffData) this.patchText(incomingDiffData)
}) })
}, },
decryptNote(){
const hashed = crypto.createHash('sha256').update(this.password).digest().toString('base64')
//Remove plaintext password
this.hashedPass = hashed
this.password = ''
this.passwordConfirm = ''
this.loadNote()
},
lockNote(){
this.save().then(results => {
this.isDecrypted = false
this.password = ''
this.hashedPass = ''
this.passwordprotect = false
this.setText('')
})
},
enableEncryption(){
if(this.noteText == ''){
this.noteText = 'Text Typed here is encrypted.'
}
const hashed = crypto.createHash('sha256').update(this.password).digest().toString('base64')
//Remove plaintext password
this.hashedPass = hashed
this.lastNoteHash = 0
this.password = ''
this.passwordConfirm = ''
this.passwordprotect = false
this.save()
.then(results => {
this.$bus.$emit('notification', 'Password Protection Enabled')
this.loadNote()
})
},
disableEncryption(){
this.lastNoteHash = 0
this.isEncrypted = false
this.password = ''
this.passwordConfirm = ''
this.hashedPass = ''
this.passwordprotect = false
//Reload Note
this.save()
.then(results => {
this.loadNote()
this.$bus.$emit('notification', 'Password Protection Removed')
})
},
titleResize(){ titleResize(){
//Resize the title field //Resize the title field
let element = this.$refs.titleTextarea let element = this.$refs.titleTextarea
@ -1345,15 +1208,6 @@
element.style.height = 'auto'; element.style.height = 'auto';
element.style.height = (element.scrollHeight + padding) +'px'; element.style.height = (element.scrollHeight + padding) +'px';
}, },
startAutolockTimer(){
//Start autolock timer on encrypted notes that are encrypted and in a decrypted state
if(this.isEncrypted && this.isDecrypted){
clearTimeout(this.autoLockTimeout)
this.autoLockTimeout = setTimeout(() => {
this.lockNote()
}, (60 * 1000 * 20) ) //Autolock after 20 min
}
},
} }
} }
</script> </script>
@ -1393,6 +1247,7 @@
background-color: var(--background_color); background-color: var(--background_color);
border: 1px solid var(--menu-accent);; border: 1px solid var(--menu-accent);;
margin: 45px 0 45px 0; margin: 45px 0 45px 0;
position: relative;
} }
/* /*
@ -1434,6 +1289,10 @@
.edit-button:hover { .edit-button:hover {
background-color: var(--menu-accent); background-color: var(--menu-accent);
} }
.edit-active {
background-color: #21BA45;
color: white;
}
.edit-divide { .edit-divide {
display: inline-block; display: inline-block;
background-color: var(--menu-accent); background-color: var(--menu-accent);
@ -1489,10 +1348,22 @@
.loading-note { .loading-note {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; width: 100%;
right: 0; height: 100%;
bottom: 0; min-height: 300px;
/*background: var(--background_color);*/
/*opacity: 0.;*/
z-index: 1;
} }
.loading-text {
margin: 0;
position: absolute;
top: 200px;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
}
/* One note open, in the middle of the screen */ /* One note open, in the middle of the screen */
.master-note-edit.position-0 { .master-note-edit.position-0 {
left: 50%; left: 50%;
@ -1502,6 +1373,10 @@
left: 15%; left: 15%;
right: 15%; right: 15%;
} }
.side-menu-open {
left: calc(50% + 10px) !important;
right: calc(0% + 10px) !important;
}
@media only screen and (max-width: 740px) { @media only screen and (max-width: 740px) {
.input-container-wrapper { .input-container-wrapper {
left: 0; left: 0;
@ -1618,6 +1493,24 @@
right: 150%; right: 150%;
} }
} }
/* Fade out transition animation */
.fade-enter {
/*opacity: 0;*/
}
.fade-enter-active {
/*transition: opacity 0.7s;*/
}
.fade-leave {
/* opacity: 0; */
}
.fade-leave-active {
transition: opacity 0.7s;
opacity: 0;
}
/* animations END */ /* animations END */
</style> </style>

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="note-title-display-card" <div class="note-title-display-card"
:style="{'background-color':color, 'color':fontColor, 'border-color':color }" :style="{'background-color':color, 'color':fontColor, 'border-color':color }"
:class="{'currently-open':currentlyOpen, 'bgboy':triggerClosedAnimation}" :class="{'currently-open':currentlyOpen, 'bgboy':triggerClosedAnimation, 'title-view':titleView }"
> >
<!-- Show title and snippet below it --> <!-- Show title and snippet below it -->
<div class="overflow-hidden note-card-text" @click="cardClicked"> <div class="overflow-hidden note-card-text" @click="cardClicked" v-if="!titleView">
<span class="subtext" v-if="note.shareUsername"> <span class="subtext" v-if="note.shareUsername">
Shared by {{ note.shareUsername }} Shared by {{ note.shareUsername }}
@ -29,12 +29,10 @@
<!-- Title display --> <!-- Title display -->
<span v-if="note.title.length > 0" <span v-if="note.title.length > 0"
data-test-id="title"
class="big-text"><p>{{ note.title }}</p></span> class="big-text"><p>{{ note.title }}</p></span>
<!-- Sub text display --> <!-- Sub text display -->
<span v-if="note.subtext.length > 0 && !isShowingSearchResults()" <span v-if="note.subtext.length > 0 && !isShowingSearchResults()"
data-test-id="subtext"
class="small-text" class="small-text"
v-html="note.subtext"></span> v-html="note.subtext"></span>
@ -62,48 +60,64 @@
</div> </div>
<div v-if="titleView" class="single-line-text" @click="cardClicked">
<span class="title-line" v-if="note.title.length > 0">{{ note.title }}<br></span>
<span class="sub-line" v-if="note.subtext.length > 0">{{ removeHtml(note.subtext) }}</span>
<span v-if="note.title.length == 0 && note.title.length == 0">Empty Note</span>
</div>
<!-- Toolbar on the bottom --> <!-- Toolbar on the bottom -->
<div class="tool-bar" @click.self="cardClicked"> <div class="tool-bar" @click.self="cardClicked" v-if="!titleView">
<div class="icon-bar"> <div class="icon-bar">
<!-- <span v-if="note.pinned == 1" data-position="top left" data-tooltip="Pinned" data-inverted>
<i class="green pin icon"></i>
</span>
<span v-if="note.archived == 1" data-position="top left" data-tooltip="Archived" data-inverted>
<i class="green archive icon"></i>
</span> -->
<span class="tags" v-if="note.tags"> <span class="tags" v-if="note.tags">
<span v-for="tag in (note.tags.split(','))" class="little-tag">{{ tag }}</span> <span v-for="tag in (note.tags.split(','))" class="little-tag">{{ tag }}</span>
<br> <br>
</span> </span>
<span data-tooltip="Edited" class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }"> <span class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
{{$helpers.timeAgo(note.updated)}} {{$helpers.timeAgo(note.updated)}}
</span> </span>
<span class="teeny-buttons" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }"> <span class="teeny-buttons" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
<i class="teeny-button" data-tooltip="Tags" data-inverted v-on:click="toggleTags(true)"> <span v-if="!note.trashed">
<i class="tags icon"></i>
</i>
<i class="teeny-button" <i class="teeny-button" data-tooltip="Tags" data-inverted v-on:click="toggleTags(true)">
data-tooltip="Archive" <i class="tags icon"></i>
:data-tooltip="note.archived ? 'Un-Archive':'Archive' " </i>
data-inverted v-on:click="archiveNote">
<i class="archive icon" :class="{'green':note.archived}"></i> <i class="teeny-button"
</i> data-tooltip="Archive"
:data-tooltip="note.archived ? 'Un-Archive':'Archive' "
data-inverted v-on:click="archiveNote">
<i class="archive icon" :class="{'green':note.archived}"></i>
</i>
<i class="teeny-button"
:data-tooltip="note.pinned ? 'Un-Pin':'Pin' "
data-inverted v-on:click="pinNote">
<i class="pin icon" :class="{'green':note.pinned}"></i>
</i>
<i class="teeny-button"
data-tooltip="Move to Trash"
data-inverted v-on:click="trashNote()">
<i class="trash icon"></i>
</i>
</span>
<!-- Trash note options -->
<span v-if="note.trashed">
<i class="teeny-button"
data-tooltip="Un-Trash"
data-inverted v-on:click="trashNote()">
<i class="reply icon"></i>
</i>
<delete-button class="teeny-button" :note-id="note.id" />
</span>
<i class="teeny-button"
:data-tooltip="note.pinned ? 'Un-Pin':'Pin' "
data-inverted v-on:click="pinNote">
<i class="pin icon" :class="{'green':note.pinned}"></i>
</i>
<delete-button class="teeny-button" :note-id="note.id" />
</span> </span>
</div> </div>
@ -113,9 +127,8 @@
<img v-for="thumb in getThumbs" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`"> <img v-for="thumb in getThumbs" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
</div> </div>
</div> </div>
</div> </div>
<side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true"> <side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true">
<div class="ui basic segment"> <div class="ui basic segment">
<note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/> <note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/>
@ -136,7 +149,7 @@
export default { export default {
name: 'NoteTitleDisplayCard', name: 'NoteTitleDisplayCard',
props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults' ], props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults', 'titleView' ],
components: { components: {
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default, 'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
'note-tag-edit': require('@/components/NoteTagEdit.vue').default, 'note-tag-edit': require('@/components/NoteTagEdit.vue').default,
@ -148,6 +161,17 @@
this.beenClicked = true this.beenClicked = true
this.onClick(this.note.id) this.onClick(this.note.id)
}, },
removeHtml(string){
if(string == undefined || string == null || string.length == 0){
return ''
}
return string
.replace(/&[[#A-Za-z0-9]+A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
.replace(/\s+/g, ' ') //Remove all whitespace
.trim()
},
cleanHighlight(text){ cleanHighlight(text){
//Basically just remove whitespace //Basically just remove whitespace
let updated = text.replace(/&nbsp;/g, '').replace(/<br>/g,'') let updated = text.replace(/&nbsp;/g, '').replace(/<br>/g,'')
@ -171,7 +195,7 @@
let postData = {'pinned': !this.note.pinned, 'noteId':this.note.id} let postData = {'pinned': !this.note.pinned, 'noteId':this.note.id}
axios.post('/api/note/setpinned', postData) axios.post('/api/note/setpinned', postData)
.then(data => { .then(data => {
this.$bus.$emit('update_single_note', this.note.id) // this.$bus.$emit('update_single_note', this.note.id)
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Pin Note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Pin Note') })
}, },
@ -183,20 +207,35 @@
//Show message so no one worries where note went //Show message so no one worries where note went
let message = 'Moved to Archive' let message = 'Moved to Archive'
if(postData.archived != 1){ if(postData.archived != 1){
message = 'Move to main list' message = 'Moved to main list'
} }
this.$bus.$emit('notification', message) this.$bus.$emit('notification', message)
this.$bus.$emit('update_single_note', this.note.id) // this.$bus.$emit('update_single_note', this.note.id)
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Archive Note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Archive Note') })
}, },
trashNote(){ //toggleArchived() <- old name
let postData = {'trashed': !this.note.trashed, 'noteId':this.note.id}
axios.post('/api/note/settrashed', postData)
.then(data => {
//Show message so no one worries where note went
let message = 'Moved to Trash'
if(postData.trashed == 0){
message = 'Moved to main list'
}
this.$bus.$emit('notification', message)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Trash Note') })
},
toggleTags(state){ toggleTags(state){
this.showTagSlideMenu = state this.showTagSlideMenu = state
if(state == false){ if(state == false){
this.$bus.$emit('update_single_note', this.note.id) // this.$bus.$emit('update_single_note', this.note.id)
} }
}, },
@ -316,6 +355,10 @@
color: var(--text_color); color: var(--text_color);
background-color: var(--background_color); background-color: var(--background_color);
} }
.subtext {
display: inline-block;
width: 100%;
}
/*Strict font sizes for card display*/ /*Strict font sizes for card display*/
.small-text { .small-text {
@ -395,6 +438,27 @@
.note-title-display-card:hover { .note-title-display-card:hover {
box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8); box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);
} }
.note-title-display-card.title-view {
width: 100%;
min-height: 10px;
max-width: none;
box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);
}
.single-line-text {
width: calc(100% - 25px);
margin: 5px 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
}
.title-line {
font-weight: bold;
font-size: 1.2em;
padding: 0 20px 0 0;
}
.icon-bar { .icon-bar {
display: inline-block; display: inline-block;
padding: 5px 10px 0; padding: 5px 10px 0;

View File

@ -6,35 +6,71 @@
right: 0; right: 0;
padding: 10px; padding: 10px;
} }
.floating-button {
position: absolute;
right: 7px;
top: 4px;
z-index: 2;
}
.floating-note-options {
position: absolute;
right: 0;
left: 0;
top: 35px;
z-index: 2;
}
.floating-note-options > .segment {
border-top: none;
border-top-right-radius: 0;
border-top-left-radius: 0;
}
</style> </style>
<template> <template>
<span> <span v-on:mouseenter="extraMenuHover = true" v-on:mouseleave="extraMenuHover = false">
<div class="ui form" v-if="!$store.getters.getIsUserOnMobile"> <div class="ui form">
<!-- normal search menu --> <!-- normal search menu -->
<div class="ui left icon fluid input"> <div class="ui left icon fluid input">
<input v-model="searchTerm" @keyup="searchKeyUp" @keyup.enter="search" placeholder="Search Notes and Files" ref="searchInput"/> <input ref="desktopSearch" v-on:blur="focused = false" v-on:focus="focused = true" v-model="searchTerm" @keydown="onKeyDown" @keyup="onKeyUp" placeholder="Search or Start Typing New Note" />
<i class="search icon"></i> <i class="search icon"></i>
</div> </div>
</div>
<div class="floating-button" v-if="searchTerm.length > 0 && !searched && searchTerm.indexOf(' ') == -1">
<div class="ui green compact button" v-on:click="search()">Search</div>
</div>
<span class="ui basic icon button" v-on:click="openFloatingSearch" v-if="$store.getters.getIsUserOnMobile"> <div class="floating-button" v-if="searchTerm.length > 0 && searched">
<i class="green search icon"></i> <div class="ui grey compact button" v-on:click="clear()">Clear</div>
</span> </div>
<div class="fixed-search" v-if="showFixedSearch"> <div class="floating-button" v-if="!searched && searchTerm.length > 0 && searchTerm.indexOf(' ') != -1">
<div class="ui raised segment"> <div class="ui grey compact button" v-on:click="searchTerm = ''">Clear</div>
<h2 class="ui center aligned header">Search!</h2> </div>
<div class="ui form">
<div class="ui left icon fluid input"> <div class="floating-note-options"
<input v-if="(searchTerm.indexOf(' ') != -1 || tagSuggestions.length > 0) && (extraMenuHover)">
ref="fixedSearch" <div class="ui segment">
v-model="searchTerm" <div class="ui very compact grid" v-if="searchTerm.indexOf(' ') != -1">
@keyup.enter="search" <div class="eight wide column">
v-on:blur="showFixedSearch = false" <div class="ui green compact shrinking button" v-on:click="pushToNewNote()">
placeholder="Press Enter to Search" /> <i class="plus icon"></i>A New Note
<i class="search icon"></i> </div>
</div>
<div class="eight wide right aligned column">
<div class="ui green compact shrinking button" v-on:click="pushToQuickNote()">
<i class="sticky note outline icon"></i>The Scratch Pad
</div>
</div>
</div>
<div class="ui very compact grid" v-if="tagSuggestions.length > 0">
<div class="sixteen wide column">
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagClick(tag.id)">
<i class="tag icon"></i>
{{ tag.text }}
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -45,14 +81,19 @@
<script> <script>
import axios from 'axios'
export default { export default {
data: function(){ data: function(){
return { return {
searchTerm: '', searchTerm:'',
searchTimeout: null, searched: false,
searchDebounceDuration: 300,
showFixedSearch: false, tagSuggestions: [],
tagSearchDebounce: null,
extraMenuHover: false,
} }
}, },
beforeCreate: function(){ beforeCreate: function(){
@ -62,31 +103,105 @@
//search clear //search clear
this.$bus.$on('reset_fast_filters', () => { this.$bus.$on('reset_fast_filters', () => {
this.searchTerm = '' this.searchTerm = ''
this.tagSuggestions = []
}) })
}, },
methods: { methods: {
openFloatingSearch(){ tagClick(tagId){
this.showFixedSearch = !this.showFixedSearch
this.$emit('tagClick', tagId)
this.tagSuggestions = []
this.searchTerm = ''
if(this.showFixedSearch){
this.$nextTick( () => {
this.searchTerm = ''
this.$refs.fixedSearch.focus()
})
}
}, },
searchKeyUp(){ clear(){
//This event is not triggered on mobile this.searched = false
clearTimeout(this.searchTimeout) this.searchTerm = ''
this.searchTimeout = setTimeout(() => { this.tagSuggestions = []
if(!this.$store.getters.getIsUserOnMobile){
this.$refs.desktopSearch.focus()
}
this.$bus.$emit('note_reload')
},
pushToQuickNote(){
const text = this.searchTerm
this.searchTerm = ''
this.tagSuggestions = []
axios.post('/api/quick-note/update', { 'pushText':text.trim() } )
.then( response => {
//Open Quick Note
if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id)
}
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Update The Scratch Pad') })
},
pushToNewNote(){
const text = this.searchTerm
this.searchTerm = ''
this.tagSuggestions = []
axios.post('/api/note/create', { text })
.then(response => {
if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id)
}
})
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') })
},
onKeyUp(event){
//Search Tags
const postData = {
'tagText':this.searchTerm.trim()
}
clearTimeout(this.tagSearchDebounce)
if(this.searchTerm.length == 0){
this.tagSuggestions = []
return
}
this.tagSearchDebounce = setTimeout(() => {
this.tagSuggestions = []
axios.post('/api/tag/suggest', postData)
.then( response => {
this.tagSuggestions = response.data
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
}, 800)
},
onKeyDown(event){
//Commant + Enter
if((event.metaKey || event.ctrlKey) && event.keyCode == 13 ){
this.pushToQuickNote()
return
}
if(event.keyCode == 13){
this.search() this.search()
}, this.searchDebounceDuration) return
}
}, },
search(){ search(){
this.searched = true
this.$refs.desktopSearch.focus()
//Blur after search on mobile
if(this.$store.getters.getIsUserOnMobile){ if(this.$store.getters.getIsUserOnMobile){
this.$refs.fixedSearch.blur() this.$refs.desktopSearch.blur()
} }
this.$bus.$emit('update_search_term', this.searchTerm) this.$bus.$emit('update_search_term', this.searchTerm)
}, },
} }

View File

@ -80,8 +80,14 @@
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Load Shared') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Load Shared') })
}, },
onRevokeAccess(noteId){ onRevokeAccess(sharedNoteId){
axios.post('/api/note/shareremoveuser', {'noteId':noteId})
const postData = {
'noteId': this.noteId,
'shareUserNoteId': sharedNoteId
}
axios.post('/api/note/shareremoveuser', postData)
.then( ({data}) => { .then( ({data}) => {
console.log(data) console.log(data)
if(data == true){ if(data == true){

View File

@ -3,7 +3,7 @@
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 55%; right: 50%;
bottom: 0; bottom: 0;
z-index: 1020; z-index: 1020;
overflow: hidden; overflow: hidden;

View File

@ -1,15 +1,15 @@
<template> <template>
<div class="ui basic button shrinking"> <div class="button-fix">
<!-- Dropdown Button --> <!-- Dropdown Button -->
<span v-if="activeTags.length == 0" v-on:click="menuOpen = true"> <span v-if="activeTags.length == 0" v-on:click="openMenu()" class="ui basic button shrinking">
<i class="green tags icon"></i> <i class="green tags icon"></i>
Tags Tags
<i class="caret down icon"></i> <i class="caret down icon"></i>
</span> </span>
<!-- Remove Tag button --> <!-- Remove Tag button -->
<span v-if="activeTags.length > 0" v-on:click="toggleActive()"> <span v-if="activeTags.length > 0" v-on:click="openMenu()" class="ui basic button shrinking">
<i class="green tag icon"></i> <i class="green tag icon"></i>
{{ getActiveTag() }} {{ getActiveTag() }}
<i class="caret right icon"></i> <i class="caret right icon"></i>
@ -18,13 +18,29 @@
<!-- hidden dropdown menu --> <!-- hidden dropdown menu -->
<div class="dropdown-menu" v-if="menuOpen"> <div class="dropdown-menu" v-if="menuOpen">
<div class="ui raised segment"> <div class="ui raised segment">
<div class="ui clickable basic label" v-for="tag in tags"> <div class="ui very tight grid">
<span v-on:click="onClick(tag.id)"> <div class="fourteen wide column">
{{ ucWords(tag.text) }} <h2 class="ui header"><i class="small green tags icon"></i>Tags</h2>
<span class="detail">{{tag.usages}}</span>
</span>
</div> </div>
<div class="two wide middle aligned center aligned column" v-on:click="menuOpen = false">
<i class="grey close icon"></i>
</div>
<div class="sixteen wide middle aligned column" v-if="loadedTags.length == 0">
Tags added to Notes will appear here.
</div>
<div class="row hover-row" v-for="tag in loadedTags" v-on:click="onClick(tag.id)" :class="{'green':(activeTags[0] == tag.id)}">
<div class="two wide center aligned column">
<i class="grey tag icon"></i>
</div>
<div class="twelve wide column">
{{ ucWords(tag.text) }}
</div>
<div class="two wide center aligned column">
{{tag.usages}}
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -34,13 +50,27 @@
</template> </template>
<script> <script>
import axios from 'axios'
export default { export default {
name: 'TagDisplay', name: 'TagDisplay',
props: [ 'tags', 'activeTags' ], props: [ 'activeTags' ],
data () {
return {
loadedTags: [],
menuOpen: false,
}
},
components: { components: {
}, },
methods:{ methods:{
openMenu(){
this.menuOpen = true
axios.post('/api/tag/usertags')
.then( ({data}) => {
this.loadedTags = data
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Tags') })
},
toggleActive(){ toggleActive(){
this.menuOpen = false this.menuOpen = false
const current = this.activeTags[0] const current = this.activeTags[0]
@ -63,7 +93,7 @@
return text return text
} }
this.tags.forEach(tag => { this.loadedTags.forEach(tag => {
if( this.activeTags.includes(tag.id) ){ if( this.activeTags.includes(tag.id) ){
text = this.ucWords(tag.text) text = this.ucWords(tag.text)
} }
@ -72,27 +102,32 @@
return text return text
}, },
}, },
data () {
return {
menuOpen: false,
}
},
beforeMount(){ beforeMount(){
} }
} }
</script> </script>
<style type="text/css"> <style type="text/css">
.button-fix {
display: inline-block;
}
.hover-row:hover {
cursor: pointer;
background-color: var(--menu-accent);
}
.dropdown-menu { .dropdown-menu {
position: absolute; position: absolute;
/*width: 70vw;*/ /*width: 70vw;*/
top: 50px;
z-index: 1005; z-index: 1005;
left: 0; left: 10px;
right: 0; right: 10px;
max-width: 600px; /*min-width: 200px;*/
/*max-width: 100%;*/
width: 340px;
text-align: left; text-align: left;
} }
.dropdown-menu .label { .dropdown-menu .button {
margin: 0 5px 5px 0; margin: 0 5px 5px 0;
} }
.shade { .shade {
@ -102,7 +137,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 1004; z-index: 1004;
background-color: transparent; background-color: #0000008a;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
} }

View File

@ -11,7 +11,7 @@
<!-- Content copied from note --> <!-- Content copied from note -->
<!-- https://www.solidscribe.com/#/notes/open/552 --> <!-- https://www.solidscribe.com/#/notes/open/552 -->
<p><b>Quick Note</b><br></p><p>The Quick note feature was designed to allow rapid input to a single note. Rather than junking up all your notes with random links, numbers or haikus, you can put them all in one place. <br></p><p>All data pushed to the quick note can still be edited like a normal note.<br></p><p><br></p><p><b>Dark Theme</b><br></p><p>Dark theme was designed to minimize the amount of blue. Less blue entering your eyes is supposed to help you fall asleep.<br></p><p>Most things turn sepia and a filter is applied to images to make them more sepia.<br></p><p>Here is some good research on the topic: <a href="https://justgetflux.com/research.html">https://justgetflux.com/research.html</a><br></p><p><br></p><p><b>Password Protected Notes</b><br></p><p>Note protected with a password are encrypted. This means the data is scrambled and unreadable unless the correct password is used to decrypt them.<br></p><p>If a password is forgotten, it can never be recovered. Passwords are not saved for encrypted notes. If you lose the password to a protected note, that note text is lost. <br></p><p>Only the text of the note is protected. Tags, Files attached to the note, and the title of the note are still visible without a password. You can not search text in a password protected note. But you can search by the title.<br></p><p><br></p><p><b>Links in notes</b><br></p><p>Links put into notes are automatically scraped. This means the data from the link will be scanned to get an image and some text from the website to help make that link more accessible in the future. <br></p><p><br></p><p><b>Files in notes</b><br></p><p>Files can be uploaded to notes. If its an image, the picture will be put into the note.<br></p><p>Images added to notes will have the text pulled out so it can be searched (This isn't super accurate so don't rely to heavily on it.) The text can be updated at any time.<br></p><p><br></p><p><b>Deleting notes</b><br></p><p>When<b> </b>notes are deleted, none of the files related to the note are deleted. <br></p><p><br></p><p><b>Daily Backups</b><br></p><p>All notes are backed up, every night, at midnight. If there is data loss, it can be restored from a backup. If you experience some sort of cataclysmic data loss please contact the system administrator for a copy of your data or a restoration procedure. <br></p> <p><b>Every Note is Encrypted</b><br></p><p>Only you can read your notes. Even if every note in the database was leaked, nothing would be readable. If the government asked for your notes, it would all be gibberish. <br></p><p><br></p><p><b>Some Data is not encrypted</b><br></p><p>Everything isn't encrypted, to keep up ease of use. Files, Tags and Attachments are not encrypted.<br></p><p><br></p><p><b>Searching is somewhat limited</b><br></p><p>Since every note is encrypted, searching is limited. To maintain security, only single words can be searched. Your search index is private and Encrypted.<br></p><p><br></p><p><b>Quick Note</b><br></p><p>The Quick note feature was designed to allow rapid input to a single note. Rather than junking up all your notes with random links, numbers or haikus, you can put them all in one place. <br></p><p>All data pushed to the quick note can still be edited like a normal note.<br></p><p><br></p><p><b>Dark Theme</b><br></p><p>Dark theme was designed to minimize the amount of blue. Less blue entering your eyes is supposed to help you fall asleep.<br></p><p>Most things turn sepia and a filter is applied to images to make them more sepia.<br></p><p>Here is some good research on the topic: <a href="https://justgetflux.com/research.html">https://justgetflux.com/research.html</a><br></p><p><br></p><p><b>Password Protected Notes</b><br></p><p>Note protected with a password are encrypted. This means the data is scrambled and unreadable unless the correct password is used to decrypt them.<br></p><p>If a password is forgotten, it can never be recovered. Passwords are not saved for encrypted notes. If you lose the password to a protected note, that note text is lost. <br></p><p>Only the text of the note is protected. Tags, Files attached to the note, and the title of the note are still visible without a password. You can not search text in a password protected note. But you can search by the title.<br></p><p><br></p><p><b>Links in notes</b><br></p><p>Links put into notes are automatically scraped. This means the data from the link will be scanned to get an image and some text from the website to help make that link more accessible in the future. <br></p><p><br></p><p><b>Files in notes</b><br></p><p>Files can be uploaded to notes. If its an image, the picture will be put into the note.<br></p><p>Images added to notes will have the text pulled out so it can be searched (This isn't super accurate so don't rely to heavily on it.) The text can be updated at any time.<br></p><p><br></p><p><b>Deleting notes</b><br></p><p>When<b> </b>notes are deleted, none of the files related to the note are deleted. <br></p><p><br></p><p><b>Daily Backups</b><br></p><p>All notes are backed up, every night, at midnight. If there is data loss, it can be restored from a backup. If you experience some sort of cataclysmic data loss please contact the system administrator for a copy of your data or a restoration procedure. <br></p>
<!-- content copied from note --> <!-- content copied from note -->
</div> </div>
@ -22,6 +22,14 @@
<script> <script>
export default { export default {
name: 'HelpPage' name: 'HelpPage',
props:[ 'message' ],
data () {
return {
items: []
}
},
methods: {
}
} }
</script> </script>

View File

@ -32,8 +32,7 @@
100%{ opacity: 0.9; } 100%{ opacity: 0.9; }
} }
.subtext { .subtext {
border-bottom: 1px solid white; text-align: center;
border-right: 1px solid white;
color: white; color: white;
font-size: 1.5rem; font-size: 1.5rem;
padding: 0 0 0 10px; padding: 0 0 0 10px;
@ -121,9 +120,8 @@
</h2> </h2>
<h3 class="subtext"> <h3 class="subtext">
Take Notes Like Never Before<i class="i cursor icon blinking"></i> An easy, encrypted Note App<i class="i cursor icon blinking"></i>
</h3> </h3>
<p class="green-text">Assuming you have never used a note application previously in your life.</p>
</div> </div>
@ -134,22 +132,21 @@
</div> </div>
<div class="row"> <div class="row">
<div class="eight wide middle aligned column">
<h2>Get Started. Only a username is required.</h2>
</div>
<div class="four wide center aligned column"> <div class="four wide center aligned column">
<router-link class="ui huge green labeled icon button" to="/login"> <router-link class="ui huge green labeled icon button" to="/login">
<i class="plug icon"></i>Register <i class="plug icon"></i>Sign Up
</router-link> </router-link>
</div> </div>
<div class="eight wide middle aligned column">
<h2>Only a Username and Password are required.</h2>
</div>
</div> </div>
<!-- set --> <!-- set -->
<div class="middle aligned centered row"> <div class="middle aligned centered row">
<div class="six wide right aligned column"> <div class="six wide right aligned column">
<h2>Everyone has knowledge that need to be expressed</h2> <h2>Solid Scribe is an online note application that focuses on ease of use and security</h2>
<h3>Utilize action potential to create notes by encoding raw brainwaves converted to written language</h3> <h3>Tools to organize and collaborate on notes while maintaining security and respecting your privacy.</h3>
</div> </div>
<div class="six wide column"> <div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" alt="Explosion of New Ideas"> <img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" alt="Explosion of New Ideas">
@ -161,29 +158,29 @@
<img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden"> <img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden">
</div> </div>
<div class="six wide column"> <div class="six wide column">
<h2>Dream it, then do it</h2> <h2>Tools to organize thousands of notes</h2>
<h3>Easily record your unlimited imagination. Ideas, stories, notes, plays, poems anything, that can reasonably be put into text</h3> <h3>Tag, Pin, Color, Archive, Attach Images and Search notes or links in notes</h3>
</div> </div>
</div> </div>
<!-- set --> <!-- set -->
<div class="middle aligned centered green row"> <div class="middle aligned centered green row">
<div class="six wide column"> <div class="six wide column">
<h2>Unbridled Input</h2> <h2>Privacy through Encryption</h2>
<h3>Revolutionary technology allows the use of any keyboard with up to 395 keys</h3> <h3>All notes are encrypted. No one can read your notes, even if they steal the data from the database.</h3>
</div> </div>
<div class="six wide column"> <div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/add.svg" alt="A shpere of newness"> <img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
</div> </div>
</div> </div>
<div class="middle aligned centered row"> <div class="middle aligned centered row">
<div class="six wide right aligned column"> <div class="six wide right aligned column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/solution.svg" alt="Hypercube of Solutions"> <img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
</div> </div>
<div class="six wide column"> <div class="six wide column">
<h2>Solutions with the Internet</h2> <h2>Extremely accessible</h2>
<h3>With the power to save any combination of letters, you can easily inscribe thoughts</h3> <h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
</div> </div>
</div> </div>
@ -194,7 +191,7 @@
<h3>Type in a word and find that same word but somewhere else</h3> <h3>Type in a word and find that same word but somewhere else</h3>
</div> </div>
<div class="six wide column"> <div class="six wide column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos"> <img loading="lazy" width="100%" src="/api/static/assets/marketing/solution.svg" alt="Hypercube of Solutions">
</div> </div>
</div> </div>
@ -242,7 +239,7 @@
<div class="middle aligned centered row"> <div class="middle aligned centered row">
<div class="six wide right aligned column"> <div class="six wide right aligned column">
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo"> <img loading="lazy" width="100%" src="/api/static/assets/marketing/add.svg" alt="A shpere of newness">
</div> </div>
<div class="six wide column"> <div class="six wide column">
<h2>Data Backups</h2> <h2>Data Backups</h2>

View File

@ -80,12 +80,17 @@
const token = response.data.token const token = response.data.token
const username = response.data.username const username = response.data.username
const masterKey = response.data.masterKey
vm.$store.commit('setLoginToken', {token, username}) 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 //Redirect user to notes section after login
vm.$router.push('/notes') this.$router.push('/notes')
} else { } else {
// this.password = ''
this.$bus.$emit('notification', 'Incorrect Username or Password') this.$bus.$emit('notification', 'Incorrect Username or Password')
vm.$store.commit('destroyLoginToken') vm.$store.commit('destroyLoginToken')
} }

View File

@ -26,31 +26,32 @@
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> --> <!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
</div> </div>
<div class="ui basic button shrinking" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['encryptedNotes'] > 0"> <div class="ui basic icon button shrinking" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
<i class="green lock alternate icon"></i>Locked <i class="trash alternate outline icon"></i>
<!-- <span>{{ $store.getters.totals['encryptedNotes'] }}</span> -->
</div> </div>
<tag-display <tag-display
v-if="commonTags.length > 0"
:tags="commonTags"
:active-tags="searchTags" :active-tags="searchTags"
v-on:tagClick="tagId => toggleTagFilter(tagId)" v-on:tagClick="tagId => toggleTagFilter(tagId)"
/> />
<div class="ui basic shrinking icon button" v-on:click="toggleTitleView()">
<i v-if="titleView" class="th icon"></i>
<i v-if="!titleView" class="bars icon"></i>
</div>
</div> </div>
<div class="six wide column" v-if="!$store.getters.getIsUserOnMobile"> <div class="six wide column">
<search-input <search-input
v-on:tagClick="tagId => toggleTagFilter(tagId)"
v-if="$store.getters.totals && $store.getters.totals['totalNotes']" /> v-if="$store.getters.totals && $store.getters.totals['totalNotes']" />
</div> </div>
<div class="eight wide column" v-if="showClear"> <div class="eight wide column" v-if="showClear">
<!-- <fast-filters /> --> <!-- <fast-filters /> -->
<span class="ui fluid green button" <span class="ui fluid green button" @click="reset">
<i class="arrow circle left icon"></i>Show All Notes
@click="reset">
<i class="arrow circle left icon"></i>Back to All Notes
</span> </span>
</div> </div>
@ -58,13 +59,34 @@
</div> </div>
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !loadingInProgress">
<h2 class="ui header">
<div class="content">
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
<div v-if="searchResultsCount == 0" class="sub header">
Search can only find key words. Try a single word search.
</div>
</div>
</h2>
</div>
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
<h2>Archived Notes</h2>
</div>
<h2 v-if="fastFilters['withLinks'] == 1">Notes with Links</h2> <div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
<h2 v-if="fastFilters['withTags'] == 1">Notes with Tags</h2> <h2 >Trash
<h2 v-if="fastFilters['onlyArchived'] == 1">Archived Notes</h2> <span>({{ $store.getters.totals['trashedNotes'] }})</span>
<h2 v-if="fastFilters['onlyShowSharedNotes'] == 1">Shared Notes</h2> <div class="ui right floated basic button" data-tooltip="This doesn't work yet">
<h2 v-if="fastFilters['onlyShowEncrypted'] == 1">Password Protected Notes</h2> <i class="poo storm icon"></i>
Empty Trash
</div>
</h2>
</div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
<h2>Shared Notes</h2>
</div>
<!-- Note title card display --> <!-- Note title card display -->
<div class="sixteen wide column"> <div class="sixteen wide column">
@ -88,12 +110,16 @@
:ref="'note-'+note.id" :ref="'note-'+note.id"
:onClick="openNote" :onClick="openNote"
:data="note" :data="note"
:title-view="titleView"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)" :currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated" :key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
/> />
</div> </div>
</div> </div>
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
</div> </div>
</div> </div>
@ -114,15 +140,11 @@
<input-notes <input-notes
v-if="activeNoteId1 != null" v-if="activeNoteId1 != null"
:key="'active_note_'+activeNoteId1"
:noteid="activeNoteId1" :noteid="activeNoteId1"
:position="activeNote1Position" :position="activeNote1Position"
:url-data="$route.params" :url-data="$route.params"
ref="note1" /> ref="note1" />
<input-notes
v-if="activeNoteId2 != null"
:noteid="activeNoteId2"
:position="activeNote2Position"
ref="note2" />
</div> </div>
</template> </template>
@ -138,30 +160,33 @@
'input-notes': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'), 'input-notes': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default, 'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
'fast-filters': require('@/components/FastFilters.vue').default, // 'fast-filters': require('@/components/FastFilters.vue').default,
'search-input': require('@/components/SearchInput.vue').default, 'search-input': require('@/components/SearchInput.vue').default,
'attachment-display': require('@/components/AttachmentDisplayCard').default, 'attachment-display': require('@/components/AttachmentDisplayCard').default,
'counter':require('@/components/AnimatedCounterComponent.vue').default, 'counter':require('@/components/AnimatedCounterComponent.vue').default,
'tag-display':require('@/components/TagDisplayComponent.vue').default, 'tag-display':require('@/components/TagDisplayComponent.vue').default,
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
}, },
data () { data () {
return { return {
initComponent: true, initComponent: true,
commonTags: [], commonTags: [],
searchTerm: '', searchTerm: '',
searchResultsCount: 0,
searchTags: [], searchTags: [],
notes: [], notes: [],
highlights: [], highlights: [],
searchDebounce: null, searchDebounce: null,
fastFilters: {}, fastFilters: {},
working: false, titleView: false,
//Load up notes in batches //Load up notes in batches
firstLoadBatchSize: 30, //First set of rapidly loaded notes firstLoadBatchSize: 10, //First set of rapidly loaded notes
batchSize: 100, //Size of batch loaded when user scrolls through current batch batchSize: 25, //Size of batch loaded when user scrolls through current batch
batchOffset: 0, //Tracks the current batch that has been loaded batchOffset: 0, //Tracks the current batch that has been loaded
loadingBatchTimeout: null, //Limit how quickly batches can be loaded loadingBatchTimeout: null, //Limit how quickly batches can be loaded
loadingInProgress: false, loadingInProgress: false,
fetchTags: false, scrollLoadEnabled: true,
//Clear button is not visible //Clear button is not visible
showClear: false, showClear: false,
@ -193,7 +218,7 @@
'sent': ['paper plane outline', 'Shared Notes'], 'sent': ['paper plane outline', 'Shared Notes'],
'notes': ['file','Notes'], 'notes': ['file','Notes'],
'highlights': ['paragraph', 'Found In Text'], 'highlights': ['paragraph', 'Found In Text'],
'locked': ['lock', 'Password Protected'] 'trashed': ['poop', 'Trashed Notes']
}, },
noteSections: { noteSections: {
pinned: [], pinned: [],
@ -202,7 +227,7 @@
sent:[], sent:[],
notes: [], notes: [],
highlights: [], highlights: [],
locked: [] trashed: []
}, },
} }
@ -211,40 +236,68 @@
this.$parent.loginGateway() this.$parent.loginGateway()
this.$io.on('new_note_created', noteId => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(noteId)
}
})
this.$io.on('note_attribute_modified', noteId => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(noteId)
}
})
//Update title cards when new note text is saved
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.updateSingleNote(noteId)
}
})
//Update totals for app //Update totals for app
this.$store.dispatch('fetchAndUpdateUserTotals') this.$store.dispatch('fetchAndUpdateUserTotals')
//Close note event
this.$bus.$on('close_active_note', ({position, noteId, modified}) => { this.$bus.$on('close_active_note', ({position, noteId, modified}) => {
this.closeNote(position) this.closeNote()
this.$store.dispatch('fetchAndUpdateUserTotals')
if(modified){ if(modified){
this.$store.dispatch('fetchAndUpdateUserTotals')
this.updateSingleNote(parseInt(noteId)) this.updateSingleNote(parseInt(noteId))
} }
}) })
this.$bus.$on('update_single_note', (noteId) => { // this.$bus.$on('update_single_note', (noteId) => {
this.updateSingleNote(noteId) // this.updateSingleNote(noteId)
}) // })
this.$bus.$on('note_deleted', (noteId) => { this.$bus.$on('note_deleted', (noteId) => {
//Remove deleted note from set, its deleted //Remove deleted note from set, its deleted
this.fetchUserTags()
Object.keys(this.noteSections).forEach( key => { Object.keys(this.noteSections).forEach( key => {
this.noteSections[key].forEach( (note, index) => { this.noteSections[key].forEach( (note, index) => {
if(note.id == noteId){ if(note.id == noteId){
this.noteSections[key].splice(index,1) this.noteSections[key].splice(index,1)
this.$store.dispatch('fetchAndUpdateUserTotals')
return return
} }
}) })
}) })
}) })
this.$bus.$on('update_fast_filters', newFilter => { this.$bus.$on('update_fast_filters', newFilter => {
this.fastFilters = newFilter this.fastFilters = newFilter
//Fast filters always return all the results and tags //Fast filters always return all the results and tags
this.search(true, this.batchSize, false).then( () => { this.search(true, this.batchSize, false).then( () => {
return this.fetchUserTags() // return
}) })
}) })
@ -254,9 +307,10 @@
this.search(true, this.batchSize) this.search(true, this.batchSize)
.then( () => { .then( () => {
this.searchAttachments() console.log('Search attachments disabled for now')
// this.searchAttachments()
return this.fetchUserTags() // return
}) })
}) })
@ -275,6 +329,7 @@
const id = this.$route.params.id const id = this.$route.params.id
this.openNote(id) this.openNote(id)
} }
window.addEventListener('scroll', this.onScroll) window.addEventListener('scroll', this.onScroll)
//Close notes when back button is pressed //Close notes when back button is pressed
@ -291,7 +346,7 @@
this.$bus.$off('note_reload') this.$bus.$off('note_reload')
this.$bus.$off('close_active_note') this.$bus.$off('close_active_note')
this.$bus.$off('update_single_note') // this.$bus.$off('update_single_note')
this.$bus.$off('note_deleted') this.$bus.$off('note_deleted')
this.$bus.$off('update_fast_filters') this.$bus.$off('update_fast_filters')
this.$bus.$off('update_search_term') this.$bus.$off('update_search_term')
@ -307,6 +362,9 @@
this.reset() this.reset()
}, },
methods: { methods: {
toggleTitleView(){
this.titleView = !this.titleView
},
showOneColumn(){ showOneColumn(){
return this.$store.getters.getIsUserOnMobile return this.$store.getters.getIsUserOnMobile
@ -323,66 +381,21 @@
if(nodeClick == 'A'){ return } if(nodeClick == 'A'){ return }
} }
//Do not open same note twice
if(this.activeNoteId1 == id || this.activeNoteId2 == id){
return;
}
//1 note open //1 note open
if(this.activeNoteId1 == null && this.activeNoteId2 == null){ if(this.activeNoteId1 == null){
this.activeNoteId1 = id this.activeNoteId1 = id
this.activeNote1Position = 0 //Middel of page this.activeNote1Position = 0 //Middel of page
this.$router.push('/notes/open/'+this.activeNoteId1) this.$router.push('/notes/open/'+this.activeNoteId1)
return return
} }
//2 notes open
if(this.activeNoteId1 != null && this.activeNoteId2 == null){
this.activeNoteId2 = id
this.activeNote1Position = 1 //Right side of page
this.activeNote2Position = 2 //Left side of page
return
}
//2 notes open
if(this.activeNoteId2 != null && this.activeNoteId1 == null){
this.activeNoteId1 = id
this.activeNote1Position = 2 //Right side of page
this.activeNote2Position = 1 //Left side of page
return
}
}, },
closeNote(position){ closeNote(position){
//One note open, close that note this.activeNoteId1 = null
if(position == 0){ this.$router.push('/notes')
this.activeNoteId1 = null
this.activeNoteId2 = null
}
//Right note closed, thats 1
if(position == 1){
this.activeNoteId1 = null
}
if(position == 2){
this.activeNoteId2 = null
}
//IF two notes get opened, update ID of open note
if(this.activeNoteId1 || this.activeNoteId2){
this.$router.push('/notes/open/'+Math.max(this.activeNoteId1, this.activeNoteId2))
} else {
//No notes are open, just show notes page
this.$router.push('/notes')
}
this.activeNote1Position = 0
this.activeNote2Position = 0
}, },
toggleTagFilter(tagId){ toggleTagFilter(tagId){
if(this.searchTags.includes(tagId)){ this.searchTags = [tagId]
this.searchTags.splice( this.searchTags.indexOf(tagId) , 1);
} else {
this.searchTags.push(tagId)
}
//Reset note set and load up notes and tags //Reset note set and load up notes and tags
if(this.searchTags.length > 0){ if(this.searchTags.length > 0){
@ -411,7 +424,7 @@
const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 ) const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 )
//If greater than 80 of the way down the page, load the next batch //If greater than 80 of the way down the page, load the next batch
if(percentageDown >= 80){ if(percentageDown >= 65 && this.scrollLoadEnabled){
this.search(false, this.batchSize, true) this.search(false, this.batchSize, true)
} }
@ -447,21 +460,20 @@
if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){ if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){
// this.$refs.note1.close() // this.$refs.note1.close()
} }
if(this.$refs.note2 && this.$refs.note2.currentNoteId == noteIdToClose){
//this.$refs.note2.close()
}
} }
}, },
visibiltyChangeAction(event){ visibiltyChangeAction(event){
//@TODO - set a timeout on this like 2 minutes or just dont do shit and update it via socket.io //Fuck this shit, just use web sockets
return
//@TODO - phase this out, update it via socket.io
//If user leaves page then returns to page, reload the first batch //If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){ if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
//Load initial batch, then tags, then other batch //Load initial batch, then tags, then other batch
this.search(false, this.firstLoadBatchSize) this.search(false, this.firstLoadBatchSize)
.then( () => { .then( () => {
return this.fetchUserTags() // return
}) })
} }
@ -471,6 +483,11 @@
// @TODO Don't even trigger this if the note wasn't changed // @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId){ updateSingleNote(noteId){
noteId = parseInt(noteId)
//Find local note, if it exists; continue
//Lookup one note using passed in ID //Lookup one note using passed in ID
const postData = { const postData = {
searchQuery: this.searchTerm, searchQuery: this.searchTerm,
@ -480,8 +497,7 @@
} }
} }
//Note data must be fetched, then sorted into existing note data
axios.post('/api/note/search', postData) axios.post('/api/note/search', postData)
.then(results => { .then(results => {
@ -508,7 +524,7 @@
//Compare note tags, if they changed, reload tags //Compare note tags, if they changed, reload tags
if(newNote.tag_count != note.tag_count){ if(newNote.tag_count != note.tag_count){
this.fetchUserTags()
} }
//go through each prop and update it with new values //go through each prop and update it with new values
@ -522,6 +538,7 @@
this.$nextTick( () => { this.$nextTick( () => {
//Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){ if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].justClosed() this.$refs['note-'+noteId][0].justClosed()
} }
@ -553,14 +570,19 @@
//Don't double load note batches //Don't double load note batches
if(this.loadingInProgress){ if(this.loadingInProgress){
console.log('Loading in progress, cancel operation')
return resolve() return resolve()
} }
//Reset a lot of stuff if we are not merging batches //Reset a lot of stuff if we are not merging batches
if(!mergeExisting){ if(!mergeExisting){
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key] = []
})
this.batchOffset = 0 // Reset batch offset if we are not merging note batches 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.commonTags = [] //Don't reset tags, if search returns tags, they will be set
} }
this.searchResultsCount = 0
//Remove all filter limits from previous queries //Remove all filter limits from previous queries
delete this.fastFilters.limitSize delete this.fastFilters.limitSize
@ -589,15 +611,25 @@
//Perform search - or die //Perform search - or die
this.loadingInProgress = true this.loadingInProgress = true
// console.time('Fetch TitleCard Batch '+notesInNextLoad)
axios.post('/api/note/search', postData) axios.post('/api/note/search', postData)
.then(response => { .then(response => {
// console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
//Save the number of notes just loaded //Save the number of notes just loaded
this.batchOffset += response.data.notes.length this.batchOffset += response.data.notes.length
//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) //Mush the two new sets of data together (set will be empty is reset is on)
if(response.data.tags.length > 0){ // if(response.data.tags.length > 0){
this.commonTags = response.data.tags // this.commonTags = response.data.tags
// }
if(response.data.total > 0){
this.searchResultsCount = response.data.total
} }
this.loadingInProgress = false this.loadingInProgress = false
@ -630,53 +662,73 @@
//Sort notes into defined sections //Sort notes into defined sections
notes.forEach(note => { notes.forEach(note => {
//Only show trashed notes when trashed
if(this.fastFilters.onlyShowTrashed == 1){
if(note.trashed == 1){
this.noteSections.trashed.push(note)
}
return
}
if(note.trashed == 1){
return
}
//Show archived notes //Show archived notes
if(note.archived == 1 && this.fastFilters.onlyArchived == 1){ if(this.fastFilters.onlyArchived == 1){
this.noteSections.archived.push(note)
if(note.pinned == 1 && note.archived == 1){
this.noteSections.pinned.push(note)
return
}
if(note.archived == 1){
this.noteSections.archived.push(note)
}
return return
} }
if(note.shareUsername != null){ if(note.archived == 1){
this.noteSections.shared.push(note)
return return
} }
//Only show sent notes section if shared is selected //Only show sent notes section if shared is selected
if(note.shared == 2 && this.fastFilters.onlyShowSharedNotes == 1){ if(this.fastFilters.onlyShowSharedNotes == 1){
this.noteSections.sent.push(note)
return if(note.shared == 2){
} this.noteSections.sent.push(note)
if(note.encrypted == 1 && this.fastFilters.onlyShowEncrypted == 1){ }
this.noteSections.locked.push(note) if(note.shareUsername != null){
return this.noteSections.shared.push(note)
} }
if(note.note_highlights.length > 0){
this.noteSections.highlights.push(note)
return return
} }
//Show shared notes on main list but not notes shared with you
if(note.shareUsername != null){ return }
// Pinned notes are always first, they can appear in the archive // Pinned notes are always first, they can appear in the archive
if(note.pinned == 1){ if(note.pinned == 1){
this.noteSections.pinned.push(note) this.noteSections.pinned.push(note)
return return
} }
//If the note is not archived, push it.
if(note.archived != 1 && this.fastFilters.onlyArchived != 1){ //Push to default note section
this.noteSections.notes.push(note) this.noteSections.notes.push(note)
}
return
}) })
}, },
reset(){ reset(){
this.showClear = false this.showClear = false
this.scrollLoadEnabled = true
this.searchTerm = '' this.searchTerm = ''
this.searchTags = [] this.searchTags = []
this.fastFilters = {} this.fastFilters = {}
this.updateFastFilters(5)
this.foundAttachments = [] //Remove all attachments this.foundAttachments = [] //Remove all attachments
this.$bus.$emit('reset_fast_filters') // this.$bus.$emit('reset_fast_filters')
//Load initial batch, then tags, then other batch //Load initial batch, then tags, then other batch
this.search(true, this.firstLoadBatchSize) this.search(true, this.firstLoadBatchSize)
.then( () => {
return this.fetchUserTags()
})
.then( () => { .then( () => {
//Load a larger batch once first batch has loaded //Load a larger batch once first batch has loaded
return this.search(false, this.batchSize, true) return this.search(false, this.batchSize, true)
@ -685,27 +737,11 @@
//Thats how you promise chain //Thats how you promise chain
}) })
}, },
fetchUserTags(){
return new Promise((resolve, reject) => {
let postData = {
searchQuery: this.searchTerm,
searchTags: this.searchTags,
fastFilters: this.fastFilters,
}
axios.post('/api/tag/usertags', postData)
.then( ({data}) => {
this.commonTags = data
resolve(data)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Tags') })
})
},
updateFastFilters(index){ updateFastFilters(index){
//clear out tags //clear out tags
this.searchTags = [] this.searchTags = []
this.loadingInProgress = false
//A little hacky, brings user to notes page then filters on click //A little hacky, brings user to notes page then filters on click
if(this.$route.name != 'Note Page'){ if(this.$route.name != 'Note Page'){
@ -720,7 +756,8 @@
'withTags', // 'Only Show Notes with Tags' 'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes' 'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes 'onlyShowSharedNotes', //Only show shared notes
'onlyShowEncrypted', 'onlyShowTrashed',
'notesHome',
] ]
let filter = {} let filter = {}

View File

@ -4,31 +4,27 @@
<div class="ui sixteen wide column"> <div class="ui sixteen wide column">
<h2 class="ui header"> <h2 class="ui header">
<i class="paper plane outline icon"></i> <i class="sticky note outline icon"></i>
<div class="content"> <div class="content">
Quick The Scratch Pad
<div class="sub header">Rapidly save text</div> <div class="sub header">One place to put random junk</div>
</div> </div>
</h2> </h2>
</div> </div>
<div class="sixteen wide middle aligned column"> <div class="sixteen wide middle aligned column">
<div class="ui compact basic button" <div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
v-on:click="enterToSubmit = !enterToSubmit"> <i class="sync alternate reload icon"></i>
<i v-if="enterToSubmit" class="green toggle on icon"></i> New Quick Note
<i v-else class="toggle off icon"></i>
<span v-if="enterToSubmit">Save after Enter press</span>
<span v-else>CTRL + Enter to Save</span>
</div> </div>
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="showNewNoteConfirm = false">
<div class="ui compact basic button" <i class="close icon"></i>
v-on:click="pasteToSubmit = !pasteToSubmit"> Cancel
<i v-if="pasteToSubmit" class="green check circle outline icon"></i> </div>
<i v-else class="circle outline icon"></i> <div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="newQuickNote()">
Save after Pasting <i class="green thumbs up icon"></i>
Confirm
</div> </div>
</div> </div>
@ -41,25 +37,28 @@
ref="fastInput" ref="fastInput"
v-model="newText" v-model="newText"
v-on:keydown="checkKeyup" v-on:keydown="checkKeyup"
v-on:paste="onPaste"
placeholder="Push to the top of the quick note." placeholder="Push to the top of the quick note."
></textarea> ></textarea>
</div> </div>
<div class="field"> <div class="field">
<div v-on:click="appendQuickNote" class="ui green button">Save</div> <div v-on:click="appendQuickNote" class="ui green button">Save (Enter)</div>
<div v-if="quickNoteId" class="ui right floated basic button" v-on:click="$router.push('/attachments/note/'+quickNoteId)"> <div v-if="quickNoteId" class="ui right floated basic button" v-on:click="$router.push('/attachments/note/'+quickNoteId)">
<i class="folder open outline icon"></i> <i class="folder open outline icon"></i>
Files Files
</div> </div>
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button"> <div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">
<i class="file outline icon"></i> <i class="file outline icon"></i>
Edit Open Note
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="fun" v-html="savedQuickNoteText"></div> <div class="one wide column"></div>
<div class="fourteen wide column">
<div class="note-card-text" v-html="savedQuickNoteText"></div>
</div>
<div class="one wide column"></div>
</div> </div>
</div> </div>
</template> </template>
@ -79,8 +78,7 @@
newText: '', newText: '',
savedQuickNoteText: '', savedQuickNoteText: '',
quickNoteId: null, quickNoteId: null,
pasteToSubmit: true, showNewNoteConfirm: false,
enterToSubmit: true,
} }
}, },
beforeCreate: function(){ beforeCreate: function(){
@ -89,6 +87,16 @@
// //
this.$parent.loginGateway() this.$parent.loginGateway()
}, },
beforeMount(){
this.$io.on('new_note_created', noteId => {
this.getQuickNote()
})
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
this.getQuickNote()
})
},
mounted: function(){ mounted: function(){
if(this.$refs.fastInput){ if(this.$refs.fastInput){
@ -107,6 +115,17 @@
} }
}, },
methods: { methods: {
newQuickNote(){
this.showNewNoteConfirm = false
axios.post('/api/quick-note/new')
.then( ({data}) => {
this.savedQuickNoteText = ''
this.quickNoteId = ''
})
},
openNoteEdit(){ openNoteEdit(){
this.$router.push({'path':'/notes/open/'+this.quickNoteId}) this.$router.push({'path':'/notes/open/'+this.quickNoteId})
}, },
@ -119,17 +138,10 @@
element.style.height = (element.scrollHeight + padding) +'px'; element.style.height = (element.scrollHeight + padding) +'px';
//Enter Key submits by default //Enter Key submits by default
if(event.keyCode == 13 && this.enterToSubmit == true){ if(event.keyCode == 13){
this.appendQuickNote() this.appendQuickNote()
return return
} }
//Alternate submit
//If command+enter or control+enter is pressed, submit
if((event.metaKey || event.ctrlKey) && [13].includes(event.keyCode) && this.enterToSubmit == false){
this.appendQuickNote()
return
}
}, },
appendQuickNote(){ appendQuickNote(){
@ -142,28 +154,17 @@
this.newText = '' //Clear text area this.newText = '' //Clear text area
this.$refs.fastInput.style.height = 'auto' //Back to normal size this.$refs.fastInput.style.height = 'auto' //Back to normal size
this.savedQuickNoteText = results.data.text
this.quickNoteId = results.data.id
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Update Quick Note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Update Quick Note') })
}, },
getQuickNote (){ getQuickNote(){
axios.post('/api/quick-note/get') axios.post('/api/quick-note/get')
.then( results => { .then( ({data}) => {
this.savedQuickNoteText = results.data.text this.savedQuickNoteText = data.text
this.quickNoteId = results.data.id this.quickNoteId = data.id
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Quick Note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Quick Note') })
}, },
onPaste(event){
if(this.pasteToSubmit == true){
setTimeout( () => {
this.appendQuickNote()
}, 10)
}
return true
},
} }
} }
</script> </script>

View File

@ -60,7 +60,7 @@ export default new Router({
{ {
path: '/quick', path: '/quick',
name: 'Quick', name: 'Quick',
meta: {title:'Quick'}, meta: {title:'Scratch Pad'},
component: QuickPage component: QuickPage
}, },
{ {

View File

@ -41,38 +41,61 @@ export default new Vuex.Store({
state.token = null state.token = null
state.username = null state.username = null
}, },
toggleNightMode(state){ toggleNightMode(state, pastTheme){
//Toggle state and save to local storage const themes = {
state.nightMode = !(state.nightMode) 'white':{
localStorage.setItem('nightMode', state.nightMode) 'background_color': '#fff',
'text_color': '#3d3d3d',
//Default theme colors 'outline_color': 'rgba(34,36,38,0.15)',
let themeColors = { 'border_color': 'rgba(34,36,38,0.20)',
'background_color': '#fff', 'menu-accent': '#cecece',
'text_color': '#3d3d3d', 'menu-text': '#5e6268',
'outline_color': 'rgba(34,36,38,0.15)', },
'border_color': 'rgba(34,36,38,0.20)', 'black':{
'menu-accent': '#cecece', 'background_color': '#000',
'menu-text': '#5e6268', 'text_color': '#FFF',
} 'outline_color': '#FFF',
//Night mode colors 'border_color': 'rgba(255, 255, 255, 0.70)',
if(state.nightMode){ 'menu-accent': '#626262',
themeColors = { 'menu-text': '#d9d9d9',
},
'night':{
'background_color': '#000', 'background_color': '#000',
'text_color': '#a98457', 'text_color': '#a98457',
'outline_color': '#a98457', 'outline_color': '#a98457',
'border_color': 'rgba(255, 255, 255, 0.31)', 'border_color': 'rgba(255, 255, 255, 0.31)',
'menu-accent': '#626262', 'menu-accent': '#626262',
'menu-text': '#d9d9d9', 'menu-text': '#d9d9d9',
} },
} }
//Catch values not in set
const totalThemes = Object.keys(themes).length
state.nightMode++
if(state.nightMode > totalThemes-1){
state.nightMode = 0
}
if(pastTheme != null){
state.nightMode = pastTheme
}
//Final catch for numbers
if(Number.isInteger(parseInt(state.nightMode)) == false){
state.nightMode = 0
}
const currentTheme = Object.keys(themes)[state.nightMode]
//Toggle state and save to local storage
localStorage.setItem('nightMode', state.nightMode)
//Go through each color and set CSS variable //Go through each color and set CSS variable
let root = document.documentElement let root = document.documentElement
Object.keys(themeColors).forEach( attribute => { Object.keys( themes[currentTheme] ).forEach( attribute => {
root.style.setProperty('--'+attribute, themeColors[attribute]) root.style.setProperty('--'+attribute, themes[currentTheme][attribute])
}) })
}, },
detectIsUserOnMobile(state){ detectIsUserOnMobile(state){
@ -87,6 +110,7 @@ export default new Vuex.Store({
})(navigator.userAgent||navigator.vendor||window.opera, state); })(navigator.userAgent||navigator.vendor||window.opera, state);
}, },
toggleNoteSettingsPane(state){ toggleNoteSettingsPane(state){
state.isNoteSettingsOpen = !state.isNoteSettingsOpen state.isNoteSettingsOpen = !state.isNoteSettingsOpen
}, },
setSocketIoSocket(state, socket){ setSocketIoSocket(state, socket){
@ -99,11 +123,11 @@ export default new Vuex.Store({
//Save all the totals for the user //Save all the totals for the user
state.userTotals = totalsObject state.userTotals = totalsObject
// console.log('-------------')
// Object.keys(totalsObject).forEach( key => { // Object.keys(totalsObject).forEach( key => {
// console.log(key + ' -- ' + totalsObject[key]) // console.log(key + ' -- ' + totalsObject[key])
// }) // })
} }
}, },
getters: { getters: {
getUsername: state => { getUsername: state => {

View File

@ -4,8 +4,8 @@ let Auth = {}
const tokenSecretKey = process.env.JSON_KEY const tokenSecretKey = process.env.JSON_KEY
Auth.createToken = (userId) => { Auth.createToken = (userId, masterKey) => {
const signedData = {'id': userId, 'date':Date.now()} const signedData = {'id':userId, 'date':Date.now(), 'masterKey':masterKey}
const token = jwt.sign(signedData, tokenSecretKey) const token = jwt.sign(signedData, tokenSecretKey)
return token return token
} }

View File

@ -69,6 +69,10 @@ CryptoString.createSalt = () => {
return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64') return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
} }
CryptoString.createSmallSalt = () => {
return crypto.randomBytes(20).toString('base64')
}
CryptoString.hash = (hashString) => { CryptoString.hash = (hashString) => {

View File

@ -37,11 +37,9 @@ var io = require('socket.io')(http, {
path:'/socket' path:'/socket'
}); });
// Make io accessible to our router //Set socket IO as a global in the app
app.use(function(req,res,next){ global.SocketIo = io
req.io = io;
next();
});
io.on('connection', function(socket){ io.on('connection', function(socket){
@ -49,7 +47,7 @@ io.on('connection', function(socket){
//When a user connects, add them to their own room //When a user connects, add them to their own room
// This allows the server to emit events to that specific user // This allows the server to emit events to that specific user
// access socket.io in the controller with req.io // access socket.io in the controller with SocketIo global
socket.on('user_connect', token => { socket.on('user_connect', token => {
Auth.decodeToken(token) Auth.decodeToken(token)
.then(userData => { .then(userData => {
@ -106,7 +104,7 @@ io.on('connection', function(socket){
http.listen(3001, function(){ http.listen(3001, function(){
console.log('socket.io liseting on port 3001'); // console.log('socket.io liseting on port 3001');
}); });
//Enable json body parsing in requests. Allows me to post data in ajax calls //Enable json body parsing in requests. Allows me to post data in ajax calls
@ -124,6 +122,7 @@ app.use(function(req, res, next){
Auth.decodeToken(token) Auth.decodeToken(token)
.then(userData => { .then(userData => {
req.headers.userId = userData.id //Update headers for the rest of the application req.headers.userId = userData.id //Update headers for the rest of the application
req.headers.masterKey = userData.masterKey
next() next()
}).catch(error => { }).catch(error => {
@ -135,17 +134,18 @@ app.use(function(req, res, next){
} }
}) })
// Testing Area
// let att = require('@models/Attachment') // Test Area
// let testUrl = 'https://dba.stackexchange.com/questions/23908/how-to-search-a-mysql-database-with-encrypted-fields' const printResults = true
// testUrl = 'https://www.solidscribe.com/#/' let UserTest = require('@models/User')
// console.log('About to scrape: ', testUrl) let NoteTest = require('@models/Note')
// att.processUrl(61, 3213, testUrl) UserTest.keyPairTest('genMan2', '1', printResults)
// .then(results => { .then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
// console.log('Scrape happened') .then( message => {
// }) if(printResults) console.log(message)
// })
// // Test Area
//Test //Test
app.get(prefix, (req, res) => res.send('The api is running')) app.get(prefix, (req, res) => res.send('The api is running'))
@ -178,4 +178,6 @@ var quickNote = require('@routes/quicknoteController')
app.use(prefix+'/quick-note', quickNote) app.use(prefix+'/quick-note', quickNote)
//Output running status //Output running status
app.listen(port, () => console.log(`Listening on port ${port}!`)) app.listen(port, () => {
// console.log(`Listening on port ${port}!`)
})

View File

@ -117,6 +117,9 @@ Attachment.update = (userId, attachmentId, updatedText, noteId) => {
Attachment.delete = (userId, attachmentId, urlDelete = false) => { Attachment.delete = (userId, attachmentId, urlDelete = false) => {
let attachment = null
let noteExists = true
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise() db.promise()
.query('SELECT * FROM attachment WHERE id = ? AND user_id = ? LIMIT 1', [attachmentId, userId]) .query('SELECT * FROM attachment WHERE id = ? AND user_id = ? LIMIT 1', [attachmentId, userId])
@ -127,21 +130,32 @@ Attachment.delete = (userId, attachmentId, urlDelete = false) => {
return resolve(true) return resolve(true)
} }
//Pull data we want out of attachment = rows[0][0]
let row = rows[0][0]
let url = row.url return db.promise().query('SELECT count(id) as `exists` FROM note WHERE id = ?', [attachment.note_id])
const noteId = row.note_id
})
.then((rows, fields) => {
noteExists = (rows[0]['exists'] > 0)
let url = attachment.url
const noteId = attachment.note_id
//Try to delete file and thumbnail //Try to delete file and thumbnail
try { try {
fs.unlinkSync(filePath+row.file_location) fs.unlinkSync(filePath+attachment.file_location)
} catch(err) { console.error('File Does not exist') } } catch(err) { console.error('File Does not exist') }
try { try {
fs.unlinkSync(filePath+'thumb_'+row.file_location) fs.unlinkSync(filePath+'thumb_'+attachment.file_location)
} catch(err) { console.error('Thumbnail Does not exist') } } catch(err) { console.error('Thumbnail Does not exist') }
//Do not delete link attachments, just hide them. They will be deleted if removed from note //Do not delete link attachments, just hide them. They will be deleted if removed from note or if note is deleted
if(row.attachment_type == 1 && !urlDelete){ if(attachment.attachment_type == 1 && !urlDelete && noteExists){
db.promise() db.promise()
.query(`UPDATE attachment SET visible = 0 WHERE id = ?`, [attachmentId]) .query(`UPDATE attachment SET visible = 0 WHERE id = ?`, [attachmentId])
.then((rows, fields) => { }) .then((rows, fields) => { })

File diff suppressed because it is too large Load Diff

View File

@ -5,45 +5,76 @@ let Note = require('@models/Note')
let QuickNote = module.exports = {} let QuickNote = module.exports = {}
QuickNote.get = (userId) => { QuickNote.get = (userId, masterKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise() db.promise()
.query(` .query(`
SELECT note.id, text FROM note SELECT note.id FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE quick_note = 1 AND user_id = ? LIMIT 1
`, [userId]) `, [userId])
.then((rows, fields) => { .then((rows, fields) => {
//Quick Note is set, return note text //Quick Note is set, return note text
if(rows[0].length == 1){ if(rows[0][0] != undefined){
resolve({ let noteId = rows[0][0].id
id: rows[0][0].id, Note.get(userId, noteId, masterKey)
text: rows[0][0].text .then( noteObject => {
return resolve(noteObject)
}) })
} else {
return resolve(null)
} }
resolve({
id: null,
text: 'Enter something to create a quick note.'
})
}) })
.catch(console.log) .catch(console.log)
}) })
} }
QuickNote.update = (userId, pushText) => { QuickNote.newNote = (userId) => {
return new Promise((resolve, reject) => {
db.promise().query('UPDATE note SET quick_note = 0 WHERE quick_note = 1 AND user_id = ?',[userId])
.then((rows, fields) => {
resolve(true)
})
})
}
QuickNote.makeUrlLink = (inputText) => {
var replacedText, replacePattern1, replacePattern2, replacePattern3;
//URLs starting with http://, https://, or ftp://
replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
replacedText = inputText.replace(replacePattern1, '<a href="$1" target="_blank">$1</a>');
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
//Change email addresses to mailto:: links.
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
return replacedText;
}
QuickNote.update = (userId, pushText, masterKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let finalId = null
let finalText = ''
//Process pushText, split at \n (new lines), put <p> tags around each new line //Process pushText, split at \n (new lines), put <p> tags around each new line
let broken = '<blockquote>' + let broken = '<p>' +
pushText.split(/\r?\n/).map( (line, index) => { pushText.split(/\r?\n/).map( (line, index) => {
let clean = line let clean = line
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities .replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
.replace(/<[^>]+>/g, '') //Rip out all HTML tags .replace(/<[^>]+>/g, '') //Rip out all HTML tags
//Turn links into actual linx
clean = QuickNote.makeUrlLink(clean)
if(clean == ''){ clean = '&nbsp;' } if(clean == ''){ clean = '&nbsp;' }
let newLine = '' let newLine = ''
if(index > 0){ newLine = '<br>' } if(index > 0){ newLine = '<br>' }
@ -51,51 +82,31 @@ QuickNote.update = (userId, pushText) => {
//Return line wrapped in p tags //Return line wrapped in p tags
return `${newLine}<span>${clean}</span>` return `${newLine}<span>${clean}</span>`
}).join('') + '</blockquote>' }).join('') + '</p><p><br></p>'
db.promise() QuickNote.get(userId, masterKey)
.query(` .then(noteObject => {
SELECT note.id, text, color, pinned, archived
FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE quick_note = 1 AND user_id = ? LIMIT 1
`, [userId])
.then((rows, fields) => {
//Quick Note is set, push it the front of existing note if(noteObject == null){
if(rows[0].length == 1){
let d = rows[0][0] //Get row data finalText += broken
//Push old text behind fresh new text return Note.create(userId, 'Quick Note', finalText, masterKey)
let newText = broken +''+ d.text .then(insertedId => {
finalId = insertedId
//Save that, then return the new text return db.promise().query('UPDATE note SET quick_note = 1 WHERE id = ? AND user_id = ?',[insertedId, userId])
Note.update(null, userId, d.id, newText, '', d.color, d.pinned, d.archived)
.then( saveResults => {
resolve({
id:d.id,
text:newText
})
}) })
} else { } else {
//Create a new note with the quick text submitted. finalText += (broken + noteObject.text)
Note.create(userId, broken, 1) finalId = noteObject.id
.then( insertId => { return Note.update(userId, noteObject.id, finalText, noteObject.title, noteObject.color, noteObject.pinned, noteObject.archived, null, masterKey)
resolve({
id:insertId,
text:broken
})
})
} }
}) })
.catch(console.log) .then( saveResults => {
return resolve(true)
})
}) })
//Lookup quick note,
//Note.create(userId, 'Quick Note', 1)
} }

View File

@ -9,79 +9,189 @@ const Note = require('@models/Note')
let ShareNote = module.exports = {} let ShareNote = module.exports = {}
// Share a note with a user, given the correct username const crypto = require('crypto')
ShareNote.addUser = (userId, noteId, rawTextId, username) => { const cs = require('@helpers/CryptoString')
ShareNote.migrateNoteToShared = (userId, noteId, shareUserId, masterKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let shareUserId = null const Note = require('@models/Note')
let newNoteShare = null const User = require('@models/User')
const cleanUser = username.toLowerCase().trim()
//Check that user actually exists //generate new random salts and password
db.promise().query(`SELECT id FROM user WHERE LOWER(username) = ?`, [cleanUser]) const sharedNoteMasterKey = cs.createSmallSalt()
let encryptedSharedKey = null //new key for note encrypted with shared users pubic key
//Current note object
let note = null
let publicKey = null
db.promise().query('SELECT id FROM user WHERE id = ?', [shareUserId])
.then((rows, fields) => { .then((rows, fields) => {
if(rows[0].length == 0){ if(rows[0].length == 0){
throw new Error('User Does Not Exist') throw new Error('User Does Not Exist')
} }
shareUserId = rows[0][0]['id'] return Note.get(userId, noteId, masterKey)
//Check if note has already been added for user
return db.promise()
.query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, rawTextId])
}) })
.then((rows, fields) => { .then( noteObject => {
if(rows[0].length != 0){ if(!noteObject){
throw new Error('User Already Has Note') throw new Error('Note Not Found')
} }
//Lookup note to share with user, clone this data to create users new note note = noteObject
return db.promise() return db.promise()
.query(`SELECT * FROM note WHERE id = ? LIMIT 1`, [noteId]) .query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, note.rawTextId])
}) })
.then((rows, fields) => { .then((rows, fields) => {
newNoteShare = rows[0][0] if(rows[0].length >= 1){
throw new Error('User Already has this note shared with them')
}
//Modify note with the share attributes we want //All check pass, proceed with sharing note
delete newNoteShare['id'] return User.getPublicKey(userId)
delete newNoteShare['opened'] })
newNoteShare['share_user_id'] = userId //User who shared the note .then( userPublicKey => {
newNoteShare['user_id'] = shareUserId //User who gets note
//Setup db colums, db values and number of '?' to put into prepared statement //Get users public key
let dbColumns = [] publicKey = userPublicKey
let dbValues = []
let escapeChars = []
//Pull out all the data we need from object to create prepared statemnt //
Object.keys(newNoteShare).forEach( key => { // Modify note to have a shared password, encrypt text with this password
escapeChars.push('?') //
dbColumns.push(key) const sharedNoteSalt = cs.createSmallSalt()
dbValues.push(newNoteShare[key])
})
//Stick all the note value back into query, insert updated note //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() return db.promise()
.query(`INSERT INTO note (${dbColumns.join()}) VALUES (${escapeChars.join()})`, dbValues) .query("UPDATE `application`.`note_raw_text` SET `text` = ?, `salt` = ? WHERE (`id` = ?)",
[encryptedText, sharedNoteSalt, note.rawTextId])
}) })
.then((rows, fields) => { .then((rows, fields) => {
//Update note share status to 2 //New Encrypted snippet, using new shared password
return db.promise() const sharedNoteSnippetSalt = cs.createSmallSalt()
.query('UPDATE note SET shared = 2 WHERE id = ?', [noteId]) 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) => { .then((rows, fields) => {
//Success!
return resolve({'success':true, shareUserId}) return User.getPublicKey(shareUserId)
})
.then(shareUserPublicKey => {
//New Encrypted snippet, using new shared password
const newSnippetSalt = cs.createSmallSalt()
const snippet = JSON.stringify([note.title, note.text.substring(0, 500)])
const encryptedSnippet = cs.encrypt(sharedNoteMasterKey, newSnippetSalt, snippet)
//Encrypt shared password for this user
const encryptedSharedKey = crypto.publicEncrypt(shareUserPublicKey, Buffer.from(sharedNoteMasterKey, 'utf8')).toString('base64')
//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])
})
.then((rows, fields) => {
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')
let success = true
return resolve({success, shareUserId, sharedUserNoteId})
}) })
.catch(error => { .catch(error => {
console.log('Shared Note Error')
console.log(error) console.log(error)
resolve(false) })
})
}
ShareNote.removeUserFromShared = (userId, noteId, shareNoteUserId, masterKey) => {
return new Promise((resolve, reject) => {
let rawTextId = null
let removeUserId = null
db.promise()
.query('SELECT note_raw_text_id, user_id FROM note WHERE id = ? AND share_user_id = ?', [shareNoteUserId, 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
//@TODO - this won't remove the note from their search index, it needs to
return Note.delete(removeUserId, shareNoteUserId)
})
.then(results => {
return db.promise().query('SELECT count(*) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
})
.then((rows, fields) => {
//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)
})
})
})
} else {
//Keep note shared
return resolve(true)
}
}) })
}) })
} }
@ -126,7 +236,7 @@ ShareNote.removeUser = (userId, noteId) => {
//Delete note entry for other user - remove users access //Delete note entry for other user - remove users access
if(removeUserId && Number.isInteger(removeUserId)){ if(removeUserId && Number.isInteger(removeUserId)){
//Delete this users access to the note //Delete this users access to the note
return Note.delete(removeUserId, noteId) return Note.delete(removeUserId, noteId, masterKey)
} else { } else {

View File

@ -20,26 +20,8 @@ Tag.userTags = (userId, searchQuery, searchTags, fastFilters) => {
WHERE note_tag.user_id = ? WHERE note_tag.user_id = ?
` `
//Show shared notes
if(fastFilters && fastFilters.onlyShowSharedNotes == 1){
query += ' AND note.share_user_id IS NOT NULL' //Show Archived
} else {
query += ' AND note.share_user_id IS NULL'
}
if(fastFilters && fastFilters.onlyShowEncrypted == 1){
query += ' AND note.encrypted = 1' //Show Archived
}
//Show archived notes, only if fast filter is set, default to not archived
if(fastFilters && fastFilters.onlyArchived == 1){
query += ' AND note.archived = 1' //Show Archived
} else {
query += ' AND note.archived = 0' //Exclude archived
}
query += ` GROUP BY tag.id query += ` GROUP BY tag.id
ORDER BY usages DESC, text ASC` ORDER BY LOWER(TRIM(text)) ASC`
db.promise() db.promise()
@ -194,16 +176,24 @@ Tag.suggest = (userId, noteId, tagText) => {
tagText += '%' tagText += '%'
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise()
.query(`SELECT text FROM note_tag let params = [userId, tagText]
let query = `SELECT tag.id, text FROM note_tag
JOIN tag ON note_tag.tag_id = tag.id JOIN tag ON note_tag.tag_id = tag.id
WHERE note_tag.user_id = ? WHERE note_tag.user_id = ?
AND tag.text LIKE ? AND tag.text LIKE ?`
AND note_tag.tag_id NOT IN (
SELECT note_tag.tag_id FROM note_tag WHERE note_tag.note_id = ? if(noteId && noteId > 0){
) params.push(noteId)
GROUP BY text query += `AND note_tag.tag_id NOT IN
LIMIT 6`, [userId, tagText, noteId]) (SELECT note_tag.tag_id FROM note_tag WHERE note_tag.note_id = ?)`
}
query += ` GROUP BY text, id LIMIT 6`
db.promise()
.query(query, params)
.then((rows, fields) => { .then((rows, fields) => {
resolve(rows[0]) //Return new ID resolve(rows[0]) //Return new ID
}) })

View File

@ -1,7 +1,10 @@
var crypto = require('crypto') const crypto = require('crypto')
let db = require('@config/database') const Note = require('@models/Note')
let Auth = require('@helpers/Auth')
const db = require('@config/database')
const Auth = require('@helpers/Auth')
const cs = require('@helpers/CryptoString')
let User = module.exports = {} let User = module.exports = {}
@ -16,32 +19,48 @@ User.login = (username, password) => {
.query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName]) .query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName])
.then((rows, fields) => { .then((rows, fields) => {
//Pull out user data from database results // Create New Account
const lookedUpUser = rows[0][0]; //
//User not found, create a new account with set data
if(rows[0].length == 0){ if(rows[0].length == 0){
User.create(lowerName, password) User.create(lowerName, password)
.then(loginToken => { .then( ({token, userId}) => {
resolve(loginToken) return resolve({ token, userId })
}) })
return
} }
//hash the password and check for a match // Login User
const salt = new Buffer(lookedUpUser.salt, 'binary') //
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){ if(rows[0].length == 1){
if(delivered_key.toString('hex') === lookedUpUser.password){
//Passback a json web token //Pull out user data from database results
const token = Auth.createToken(lookedUpUser.id) const lookedUpUser = rows[0][0]
resolve(token)
} else { //hash the password and check for a match
// const salt = new Buffer(lookedUpUser.salt, 'binary')
const salt = Buffer.from(lookedUpUser.salt, 'binary')
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
if(delivered_key.toString('hex') === lookedUpUser.password){
reject('Password does not match database') User.generateMasterKey(lookedUpUser.id, password)
} .then( result => User.getMasterKey(lookedUpUser.id, password))
}) .then(masterKey => {
User.generateKeypair(lookedUpUser.id, masterKey)
.then(({publicKey, privateKey}) => {
//Passback a json web token
const token = Auth.createToken(lookedUpUser.id, masterKey)
resolve({ token: token, userId:lookedUpUser.id })
})
})
} else {
reject('Password does not match database')
}
})
}
}) })
.catch(console.log) .catch(console.log)
@ -71,7 +90,7 @@ User.create = (username, password) => {
shasum.update(''+otherRandomInt) //Update Hasd shasum.update(''+otherRandomInt) //Update Hasd
const saltString = shasum.digest('hex') const saltString = shasum.digest('hex')
const salt = new Buffer(saltString, 'binary') //Generate Salt hash const salt = Buffer.from(saltString, 'binary') //Generate Salt hash
const iterations = 25000 const iterations = 25000
crypto.pbkdf2(password, salt, iterations, 512, 'sha512', function(err, delivered_key) { crypto.pbkdf2(password, salt, iterations, 512, 'sha512', function(err, delivered_key) {
@ -87,25 +106,33 @@ User.create = (username, password) => {
created: currentDate created: currentDate
}; };
let userId = null
let newMasterKey = null
db.promise() db.promise()
.query('INSERT INTO user SET ?', new_user) .query('INSERT INTO user SET ?', new_user)
.then((rows, fields) => { .then((rows, fields) => {
if(rows[0].affectedRows == 1){ userId = rows[0].insertId
return User.generateMasterKey(userId, password)
})
.then( result => {
const newUserId = rows[0].insertId return User.getMasterKey(userId, password)
const loginToken = Auth.createToken(newUserId) })
resolve(loginToken) .then(masterKey => {
newMasterKey = masterKey
return User.generateKeypair(userId, newMasterKey)
})
.then(({publicKey, privateKey}) => {
} else { const token = Auth.createToken(userId, newMasterKey)
//Emit Error to user return resolve({token, userId})
reject('New user could not be created')
}
}) })
.catch(console.log) .catch(console.log)
}) })
} else { } else {
reject('Username already in use.') return reject('Username already in use.')
}//END user create }//END user create
}) })
.catch(console.log) .catch(console.log)
@ -122,22 +149,24 @@ User.getCounts = (userId) => {
db.promise().query( db.promise().query(
`SELECT `SELECT
SUM(pinned = 1 && archived = 0 && share_user_id IS NULL) AS pinnedNotes, SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes,
SUM(archived = 1 && share_user_id IS NULL) AS archivedNotes, SUM(trashed = 1) AS trashedNotes,
SUM(encrypted = 1) AS encryptedNotes, SUM(share_user_id IS NULL && trashed = 0) AS totalNotes,
SUM(share_user_id IS NULL) AS totalNotes, SUM(share_user_id != ? && trashed = 0) AS sharedToNotes,
SUM(share_user_id != ?) 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 != ? && opened IS null) || (share_user_id != ? && note_raw_text.updated > opened) ) AS unreadNotes
FROM note FROM note
LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE user_id = ?`, [userId, userId, userId, userId]) WHERE user_id = ?`, [userId, userId, userId, userId])
.then( (rows, fields) => { .then( (rows, fields) => {
Object.assign(countTotals, rows[0][0]) //combine results Object.assign(countTotals, rows[0][0]) //combine results
//
// @TODO - Figured out if this is useful
// We want, notes shared with user and note user has shared
//
return db.promise().query( return db.promise().query(
`SELECT count(id) AS sharedFromNotes `SELECT count(id) AS sharedFromNotes
FROM note WHERE share_user_id = ?`, [userId] FROM note WHERE shared = 2 AND user_id = ? AND trashed = 0`, [userId]
) )
}) })
.then( (rows, fields) => { .then( (rows, fields) => {
@ -168,3 +197,249 @@ User.getCounts = (userId) => {
}) })
} }
User.generateMasterKey = (userId, password) => {
return new Promise((resolve, reject) => {
if(!userId || !password){
reject('Need userId and password to generate key')
}
db.promise()
.query('SELECT count(id) as total FROM user_key WHERE user_id = ?', [userId])
.then((rows, fields) => {
//Entry already exists, you good.
if(rows[0][0]['total'] > 0){
return resolve(true)
// throw new Error('User Encryption key already exists')
} else {
// Generate user key, its big and random
const masterPassword = cs.createSmallSalt()
//Generate a salt because it wants it
const salt = cs.createSmallSalt()
// Encrypt master password
const encryptedMasterPassword = cs.encrypt(password, salt, masterPassword)
const created = Math.round((+new Date)/1000)
db.promise()
.query(
'INSERT INTO user_key (`user_id`, `salt`, `key`, `created`) VALUES (?, ?, ?, ?);',
[userId, salt, encryptedMasterPassword, created]
)
.then(results => {
return resolve(true)
})
}
})
.catch(error => {
console.log('Create Master Password Error')
console.log(error)
})
})
}
User.getMasterKey = (userId, password) => {
if(!userId || !password){
reject('Need userId and password to fetch key')
}
return new Promise((resolve, reject) => {
db.promise().query('SELECT * FROM user_key WHERE user_id = ? LIMIT 1', [userId])
.then((rows, fields) => {
const row = rows[0][0]
if(!rows[0] || rows[0].length == 0 || rows[0][0] == undefined){
return reject('Row or salt or something not set')
}
const masterKey = cs.decrypt(password, row['salt'], row['key'])
if(masterKey == null){
return reject('Unable to decrypt key')
}
return resolve(masterKey)
})
})
}
User.generateKeypair = (userId, masterKey) => {
let publicKey = null
let privateKey = null
return new Promise((resolve, reject) => {
db.promise().query('SELECT * FROM user_key WHERE user_id = ?', [userId])
.then((rows, fields) => {
const row = rows[0][0]
const salt = row['salt']
publicKey = row['public_key']
privateKey = row['private_key_encrypted']
if(row['public_key'] == null){
const keyPair = crypto.generateKeyPairSync('rsa', {
modulusLength: 1024,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
})
publicKey = keyPair.publicKey
privateKey = keyPair.privateKey
const privateKeyEncrypted = cs.encrypt(masterKey, salt, privateKey)
db.promise()
.query(
'UPDATE user_key SET `public_key` = ?, `private_key_encrypted` = ? WHERE user_id = ?;',
[publicKey, privateKeyEncrypted, userId]
)
.then((rows, fields)=>{
return resolve({publicKey, privateKey})
})
} else {
//Decrypt private key
privateKey = cs.decrypt(masterKey, salt, privateKey)
return resolve({publicKey, privateKey})
}
})
})
}
User.getPublicKey = (userId) => {
return new Promise((resolve, reject) => {
db.promise().query('SELECT public_key FROM user_key WHERE user_id = ?', [userId])
.then((rows, fields) => {
const row = rows[0][0]
return resolve(row['public_key'])
})
})
}
User.getPrivateKey = (userId, masterKey) => {
return new Promise((resolve, reject) => {
db.promise().query('SELECT salt, private_key_encrypted FROM user_key WHERE user_id = ?', [userId])
.then((rows, fields) => {
const row = rows[0][0]
const salt = row['salt']
privateKey = row['private_key_encrypted']
//Decrypt private key
privateKey = cs.decrypt(masterKey, salt, privateKey)
return resolve(privateKey)
})
})
}
User.getByUserName = (username) => {
return new Promise((resolve, reject) => {
db.promise().query('SELECT * FROM user WHERE username = ? LIMIT 1', [username.toLowerCase()])
.then((rows, fields) => {
resolve(rows[0][0])
})
})
}
User.deleteUser = (userId, password) => {
//Verify user is correct by decryptig master key with password
let deletePromises = []
let noteDelete = db.promise().query(`
DELETE note, note_raw_text
FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE note.user_id = ?
`,[userId])
deletePromises.push(noteDelete)
let userDelete = db.promise().query(`
DELETE FROM user WHERE id = ?
`,[userId])
deletePromises.push(userDelete)
let tables = ['user_key', 'user_encrypted_search_index', 'attachment']
tables.forEach(tableName => {
const query = `DELETE FROM ${tableName} WHERE user_id = ?`
const deleteQuery = db.promise().query(query, [userId])
deletePromises.push(deleteQuery)
})
return Promise.all(deletePromises)
}
User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
return new Promise((resolve, reject) => {
let masterKey = null
let testUserId = null
const randomUsername = Math.random().toString(36).substring(2, 15);
const randomPassword = '1'
User.login(testUserName, password)
.then( ({ token, userId }) => {
testUserId = userId
if(printResults) console.log('Test: Create/Login User '+testUserName+' - Pass')
return User.getMasterKey(testUserId, password)
})
.then(newMasterKey => {
masterKey = newMasterKey
if(printResults) console.log('Test: Generate/Decrypt Master Key - Pass')
return User.generateKeypair(testUserId, masterKey)
})
.then(({publicKey, privateKey}) => {
const publicKeyMessage = 'Test: Public key decrypt - Pass'
const privateKeyMessage = 'Test: Private key decrypt - Pass'
//Encrypt Message with private Key
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
//Conver back to a string
if(printResults) console.log(decryptedPrivate.toString('utf8'))
//Encrypt with public key
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
//Convert it back to string
if(printResults) console.log(publicDeccryptMessage.toString('utf8'))
resolve({testUserId, masterKey})
})
})
}

View File

@ -1,19 +1,18 @@
var express = require('express') var express = require('express')
var router = express.Router() var router = express.Router()
let Notes = require('@models/Note'); let Note = require('@models/Note')
let ShareNote = require('@models/ShareNote'); let User = require('@models/User')
let ShareNote = require('@models/ShareNote')
let userId = null let userId = null
let socket = null let masterKey = null
// middleware that is specific to this router // middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function setUserId (req, res, next) {
if(req.headers.userId){ if(req.headers.userId){
userId = req.headers.userId userId = req.headers.userId
} masterKey = req.headers.masterKey
if(req.headers.socket){
// socket = req.
} }
next() next()
@ -23,56 +22,67 @@ router.use(function setUserId (req, res, next) {
// Note actions // Note actions
// //
router.post('/get', function (req, res) { router.post('/get', function (req, res) {
// req.io.emit('welcome_homie', 'Welcome, dont poop from excitement') Note.get(userId, req.body.noteId, masterKey)
Notes.get(userId, req.body.noteId, req.body.password)
.then( data => { .then( data => {
//Join room when user opens note
// req.io.join('note_room')
res.send(data) res.send(data)
}) })
}) })
router.post('/delete', function (req, res) { router.post('/delete', function (req, res) {
Notes.delete(userId, req.body.noteId) Note.delete(userId, req.body.noteId)
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })
router.post('/create', function (req, res) { router.post('/create', function (req, res) {
Notes.create(userId, req.body.title, req.body.text) Note.create(userId, req.body.title, req.body.text, masterKey)
.then( id => res.send({id}) ) .then( id => res.send({id}) )
}) })
router.post('/update', function (req, res) { router.post('/update', function (req, res) {
Notes.update(req.io, userId, req.body.noteId, req.body.text, req.body.title, req.body.color, req.body.pinned, req.body.archived, req.body.password, req.body.hint) 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}) ) .then( id => res.send({id}) )
}) })
router.post('/search', function (req, res) { router.post('/search', function (req, res) {
Notes.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters) Note.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters, masterKey)
.then( notesAndTags => { .then( NoteAndTags => {
res.send(notesAndTags) res.send(NoteAndTags)
}) })
}) })
router.post('/difftext', function (req, res) { router.post('/difftext', function (req, res) {
Notes.getDiffText(userId, req.body.noteId, req.body.text, req.body.updated) Note.getDiffText(userId, req.body.noteId, req.body.text, req.body.updated)
.then( fullDiffText => { .then( fullDiffText => {
//Response should be full diff text //Response should be full diff text
res.send(fullDiffText) res.send(fullDiffText)
}) })
}) })
router.post('/reindex', function (req, res) {
Note.reindex(userId, masterKey)
.then( data => {
res.send(data)
})
})
// //
// Update single note attributes // Update single note attributes
// //
router.post('/setpinned', function (req, res) { router.post('/setpinned', function (req, res) {
Notes.setPinned(userId, req.body.noteId, req.body.pinned) Note.setPinned(userId, req.body.noteId, req.body.pinned)
.then( results => { .then( results => {
res.send(results) res.send(results)
}) })
}) })
router.post('/setarchived', function (req, res) { router.post('/setarchived', function (req, res) {
Notes.setArchived(userId, req.body.noteId, req.body.archived) Note.setArchived(userId, req.body.noteId, req.body.archived)
.then( results => {
res.send(results)
})
})
router.post('/settrashed', function (req, res) {
Note.setTrashed(userId, req.body.noteId, req.body.trashed)
.then( results => { .then( results => {
res.send(results) res.send(results)
}) })
@ -87,18 +97,20 @@ router.post('/getshareusers', function (req, res) {
}) })
router.post('/shareadduser', function (req, res) { router.post('/shareadduser', function (req, res) {
ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username) // 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)
})
.then( ({success, shareUserId}) => { .then( ({success, shareUserId}) => {
//Emit update count event to user shared with - so they see the note in real time
req.io.to(shareUserId).emit('update_counts')
res.send(success) res.send(success)
}) })
}) })
router.post('/shareremoveuser', function (req, res) { router.post('/shareremoveuser', function (req, res) {
ShareNote.removeUser(userId, req.body.noteId) // (userId, noteId, shareNoteUserId, shareUserId, masterKey)
ShareNote.removeUserFromShared(userId, req.body.noteId, req.body.shareUserNoteId, masterKey)
.then(results => res.send(results)) .then(results => res.send(results))
}) })
@ -106,15 +118,14 @@ router.post('/shareremoveuser', function (req, res) {
// //
// Testing Action // Testing Action
// //
//Reindex all notes. Not a very good function, not public //Reindex all Note. Not a very good function, not public
router.get('/reindex5yu43prchuj903mrc', function (req, res) { router.get('/reindex5yu43prchuj903mrc', function (req, res) {
Notes.migrateNoteTextToNewTable().then(status => { Note.migrateNoteTextToNewTable().then(status => {
return res.send(status) return res.send(status)
}) })
}) })
module.exports = router module.exports = router

View File

@ -2,12 +2,15 @@ var express = require('express')
var router = express.Router() var router = express.Router()
let QuickNote = require('@models/QuickNote'); let QuickNote = require('@models/QuickNote');
let userId = null let userId = null
let masterKey = null
// middleware that is specific to this router // middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function setUserId (req, res, next) {
if(userId = req.headers.userId){ if(userId = req.headers.userId){
userId = req.headers.userId userId = req.headers.userId
masterKey = req.headers.masterKey
} }
next() next()
@ -15,19 +18,19 @@ router.use(function setUserId (req, res, next) {
//Get quick note text //Get quick note text
router.post('/get', function (req, res) { router.post('/get', function (req, res) {
QuickNote.get(userId) QuickNote.get(userId, masterKey)
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })
//Push text to quick note //Push text to quick note
router.post('/update', function (req, res) { router.post('/update', function (req, res) {
QuickNote.update(userId, req.body.pushText) QuickNote.update(userId, req.body.pushText, masterKey)
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })
//Change quick note to a new note //Push text to quick note
router.post('/change', function (req, res) { router.post('/new', function (req, res) {
QuickNote.change(userId, req.body.noteId) QuickNote.newNote(userId)
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })

View File

@ -2,6 +2,7 @@ var express = require('express')
var router = express.Router() var router = express.Router()
let User = require('@models/User'); let User = require('@models/User');
const cs = require('@helpers/CryptoString')
// middleware that is specific to this router // middleware that is specific to this router
router.use(function timeLog (req, res, next) { router.use(function timeLog (req, res, next) {
@ -31,19 +32,19 @@ router.post('/login', function (req, res) {
} }
User.login(username, password) User.login(username, password)
.then(function(loginToken){ .then( ({token, userId}) => {
//Return json web token to user returnData['username'] = username
returnData['success'] = true returnData['token'] = token
returnData['token'] = loginToken returnData['success'] = true
returnData['username'] = username
res.send(returnData) res.send(returnData)
}) return
.catch(e => { })
console.log(e) .catch(e => {
res.send(returnData) console.log(e)
}) res.send(returnData)
})
}) })
// fetch counts of users notes // fetch counts of users notes