* 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
This commit is contained in:
parent
df073b0e4d
commit
67b218329b
@ -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>
|
|
@ -199,11 +199,11 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="paper plane outline icon"></i>Quick Note
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<div class="menu-section" v-if="!loggedIn">
|
<div class="menu-section" v-if="!loggedIn">
|
||||||
<router-link v-if="!loggedIn" class="menu-item menu-button" exact-active-class="active" to="/">
|
<router-link v-if="!loggedIn" class="menu-item menu-button" exact-active-class="active" to="/">
|
||||||
@ -257,7 +257,7 @@
|
|||||||
},
|
},
|
||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
version: '1.0.5',
|
version: '2.1.0',
|
||||||
username: '',
|
username: '',
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
mobile: false,
|
mobile: false,
|
||||||
|
67
client/src/components/LoadingIconComponent.vue
Normal file
67
client/src/components/LoadingIconComponent.vue
Normal 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?'#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?'#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>
|
@ -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 -->
|
||||||
@ -101,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)}`">
|
||||||
@ -110,19 +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)}" >
|
||||||
|
|
||||||
<!-- Loading indicator -->
|
|
||||||
<div v-if="loading" class="loading-note">
|
|
||||||
<div class="loading-text">
|
|
||||||
Decrypting Note &
|
|
||||||
{{loadingMessage}}
|
|
||||||
</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"
|
||||||
@ -134,45 +137,8 @@
|
|||||||
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>
|
||||||
|
|
||||||
@ -287,11 +253,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,
|
||||||
@ -316,12 +284,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
|
||||||
@ -329,23 +292,12 @@
|
|||||||
// 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
|
|
||||||
passwordHint: '',
|
|
||||||
password: '', //Field Variables, only for form
|
|
||||||
passwordConfirm: '', //Only a form variable
|
|
||||||
hashedPass: '', //sha-256 password hash, sends to server for decryption
|
|
||||||
isEncrypted: false,
|
|
||||||
isDecrypted: false,
|
|
||||||
passwordprotect: false,
|
|
||||||
decryptAttempts: 0,
|
|
||||||
lockedOut: false,
|
|
||||||
autoLockTimeout: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -372,9 +324,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
|
||||||
|
|
||||||
@ -382,7 +334,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -399,22 +351,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(() => {
|
||||||
@ -429,6 +382,9 @@
|
|||||||
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)
|
||||||
|
|
||||||
|
this.lastNoteHash = this.hashString(this.getText())
|
||||||
|
console.log('hash on load', this.lastNoteHash)
|
||||||
|
|
||||||
//focus on open, not on mobile, thats annoying
|
//focus on open, not on mobile, thats annoying
|
||||||
if(!this.$store.getters.getIsUserOnMobile){
|
if(!this.$store.getters.getIsUserOnMobile){
|
||||||
// this.editor.focus()
|
// this.editor.focus()
|
||||||
@ -826,12 +782,12 @@
|
|||||||
|
|
||||||
//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
|
||||||
}
|
}
|
||||||
@ -840,7 +796,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
|
||||||
@ -852,7 +807,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)
|
||||||
@ -866,29 +820,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()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
@ -1064,7 +1001,7 @@
|
|||||||
},
|
},
|
||||||
onKeyup(){
|
onKeyup(){
|
||||||
|
|
||||||
this.statusText = 'Save'
|
this.statusText = ''
|
||||||
|
|
||||||
this.diffText()
|
this.diffText()
|
||||||
|
|
||||||
@ -1088,23 +1025,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 )
|
||||||
|
if( this.lastNoteHash == currentHash){
|
||||||
this.statusText = 'Saved'
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1115,10 +1045,11 @@
|
|||||||
'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'
|
||||||
@ -1126,8 +1057,8 @@
|
|||||||
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') })
|
||||||
@ -1135,7 +1066,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'){
|
||||||
@ -1169,18 +1102,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;
|
||||||
@ -1217,6 +1147,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
|
||||||
@ -1231,62 +1166,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
|
||||||
@ -1295,15 +1174,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>
|
||||||
@ -1343,6 +1213,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -1438,18 +1309,18 @@
|
|||||||
}
|
}
|
||||||
.loading-note {
|
.loading-note {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20%;
|
top: 0;
|
||||||
left: 20%;
|
width: 100%;
|
||||||
right: 20%;
|
height: 100%;
|
||||||
bottom: 20%;
|
min-height: 300px;
|
||||||
background: transparent;
|
background: var(--background_color);
|
||||||
color: #5e6268;;
|
/*opacity: 0.;*/
|
||||||
font-size: 1.3em;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.loading-text {
|
.loading-text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 200px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-right: -50%;
|
margin-right: -50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
@ -1464,6 +1335,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;
|
||||||
@ -1580,6 +1455,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>
|
@ -6,6 +6,12 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
.floating-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 4px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
@ -13,12 +19,18 @@
|
|||||||
<div class="ui form" v-if="!$store.getters.getIsUserOnMobile">
|
<div class="ui form" v-if="!$store.getters.getIsUserOnMobile">
|
||||||
<!-- 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.enter="search" placeholder="Search Notes and Files" ref="searchInput"/>
|
<input ref="desktopSearch" v-model="searchTerm" @keyup.enter="search" placeholder="Search Notes and Files" />
|
||||||
<i class="search icon"></i>
|
<i class="search icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="floating-button" v-if="searchTerm.length > 0 && !searched">
|
||||||
|
<div class="ui green compact button" v-on:click="search()">Search</div>
|
||||||
|
</div>
|
||||||
|
<div class="floating-button" v-if="searchTerm.length > 0 && searched">
|
||||||
|
<div class="ui grey compact button" v-on:click="clear()">Clear</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Only show button on mobile -->
|
||||||
<span class="ui basic icon button" v-on:click="openFloatingSearch" v-if="$store.getters.getIsUserOnMobile">
|
<span class="ui basic icon button" v-on:click="openFloatingSearch" v-if="$store.getters.getIsUserOnMobile">
|
||||||
<i class="green search icon"></i>
|
<i class="green search icon"></i>
|
||||||
</span>
|
</span>
|
||||||
@ -50,9 +62,8 @@
|
|||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
searchTimeout: null,
|
|
||||||
searchDebounceDuration: 300,
|
|
||||||
showFixedSearch: false,
|
showFixedSearch: false,
|
||||||
|
searched: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeCreate: function(){
|
beforeCreate: function(){
|
||||||
@ -76,17 +87,22 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
searchKeyUp(){
|
clear(){
|
||||||
//This event is not triggered on mobile
|
this.searched = false
|
||||||
clearTimeout(this.searchTimeout)
|
this.searchTerm = ''
|
||||||
this.searchTimeout = setTimeout(() => {
|
if(!this.$store.getters.getIsUserOnMobile){
|
||||||
this.search()
|
this.$refs.desktopSearch.focus()
|
||||||
}, this.searchDebounceDuration)
|
}
|
||||||
|
this.$bus.$emit('note_reload')
|
||||||
},
|
},
|
||||||
search(){
|
search(){
|
||||||
|
this.searched = true
|
||||||
if(this.$store.getters.getIsUserOnMobile){
|
if(this.$store.getters.getIsUserOnMobile){
|
||||||
this.$refs.fixedSearch.blur()
|
this.$refs.fixedSearch.blur()
|
||||||
}
|
}
|
||||||
|
if(!this.$store.getters.getIsUserOnMobile){
|
||||||
|
this.$refs.desktopSearch.focus()
|
||||||
|
}
|
||||||
this.$bus.$emit('update_search_term', this.searchTerm)
|
this.$bus.$emit('update_search_term', this.searchTerm)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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,26 @@
|
|||||||
<!-- 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="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 +47,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 +90,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 +99,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 +134,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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -26,14 +26,7 @@
|
|||||||
<!-- <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">
|
|
||||||
<i class="green lock alternate icon"></i>Locked
|
|
||||||
<!-- <span>{{ $store.getters.totals['encryptedNotes'] }}</span> -->
|
|
||||||
</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)"
|
||||||
/>
|
/>
|
||||||
@ -47,10 +40,8 @@
|
|||||||
|
|
||||||
<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,7 +49,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 v-if="searchTerm.length > 0 && !loadingInProgress">
|
||||||
|
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
|
||||||
|
</h2>
|
||||||
|
|
||||||
<h2 v-if="fastFilters['withLinks'] == 1">Notes with Links</h2>
|
<h2 v-if="fastFilters['withLinks'] == 1">Notes with Links</h2>
|
||||||
<h2 v-if="fastFilters['withTags'] == 1">Notes with Tags</h2>
|
<h2 v-if="fastFilters['withTags'] == 1">Notes with Tags</h2>
|
||||||
@ -94,6 +87,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -143,12 +139,14 @@
|
|||||||
'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: [],
|
||||||
@ -161,7 +159,6 @@
|
|||||||
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,
|
scrollLoadEnabled: true,
|
||||||
|
|
||||||
//Clear button is not visible
|
//Clear button is not visible
|
||||||
@ -193,8 +190,7 @@
|
|||||||
'shared': ['envelope outline', 'Received Notes'],
|
'shared': ['envelope outline', 'Received Notes'],
|
||||||
'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']
|
|
||||||
},
|
},
|
||||||
noteSections: {
|
noteSections: {
|
||||||
pinned: [],
|
pinned: [],
|
||||||
@ -202,8 +198,7 @@
|
|||||||
shared:[],
|
shared:[],
|
||||||
sent:[],
|
sent:[],
|
||||||
notes: [],
|
notes: [],
|
||||||
highlights: [],
|
highlights: []
|
||||||
locked: []
|
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -212,6 +207,13 @@
|
|||||||
|
|
||||||
this.$parent.loginGateway()
|
this.$parent.loginGateway()
|
||||||
|
|
||||||
|
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
|
||||||
|
//Do not update note if its open
|
||||||
|
if(this.activeNoteId1 != noteId){
|
||||||
|
console.log('notePage: update display of note ', noteId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
//Update totals for app
|
//Update totals for app
|
||||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||||
|
|
||||||
@ -230,7 +232,7 @@
|
|||||||
|
|
||||||
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){
|
||||||
@ -245,7 +247,7 @@
|
|||||||
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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -258,7 +260,7 @@
|
|||||||
console.log('Search attachments disabled for now')
|
console.log('Search attachments disabled for now')
|
||||||
// this.searchAttachments()
|
// this.searchAttachments()
|
||||||
|
|
||||||
return this.fetchUserTags()
|
// return
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -381,11 +383,7 @@
|
|||||||
},
|
},
|
||||||
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){
|
||||||
@ -458,13 +456,16 @@
|
|||||||
},
|
},
|
||||||
visibiltyChangeAction(event){
|
visibiltyChangeAction(event){
|
||||||
|
|
||||||
|
//Fuck this shit, just use web sockets
|
||||||
|
return
|
||||||
|
|
||||||
//@TODO - phase this out, update it via socket.io
|
//@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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -511,7 +512,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
|
||||||
@ -556,14 +557,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
|
||||||
@ -592,11 +598,11 @@
|
|||||||
|
|
||||||
//Perform search - or die
|
//Perform search - or die
|
||||||
this.loadingInProgress = true
|
this.loadingInProgress = true
|
||||||
console.time('Fetch TitleCard Batch '+notesInNextLoad)
|
// 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)
|
// 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
|
||||||
@ -605,8 +611,12 @@
|
|||||||
this.scrollLoadEnabled = response.data.notes.length > 0
|
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
|
||||||
@ -653,10 +663,6 @@
|
|||||||
this.noteSections.sent.push(note)
|
this.noteSections.sent.push(note)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(note.encrypted == 1 && this.fastFilters.onlyShowEncrypted == 1){
|
|
||||||
this.noteSections.locked.push(note)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(note.note_highlights.length > 0){
|
if(note.note_highlights.length > 0){
|
||||||
this.noteSections.highlights.push(note)
|
this.noteSections.highlights.push(note)
|
||||||
return
|
return
|
||||||
@ -685,9 +691,8 @@
|
|||||||
//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( () => {
|
.then( () => {
|
||||||
return this.fetchUserTags()
|
|
||||||
})
|
|
||||||
.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)
|
||||||
})
|
})
|
||||||
@ -695,23 +700,6 @@
|
|||||||
//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
|
||||||
|
@ -106,7 +106,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
|
||||||
@ -139,6 +139,11 @@ app.use(function(req, res, next){
|
|||||||
|
|
||||||
// Test Area
|
// Test Area
|
||||||
// -> right here
|
// -> right here
|
||||||
|
let UserTest = require('@models/User')
|
||||||
|
let NoteTest = require('@models/Note')
|
||||||
|
// UserTest.keyPairTest()
|
||||||
|
// .then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey))
|
||||||
|
// .then( message => { console.log(message) })
|
||||||
// Test Area
|
// Test Area
|
||||||
|
|
||||||
|
|
||||||
@ -173,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}!`)
|
||||||
|
})
|
@ -1,5 +1,7 @@
|
|||||||
let db = require('@config/database')
|
let db = require('@config/database')
|
||||||
|
|
||||||
|
let Note = module.exports = {}
|
||||||
|
|
||||||
let Tags = require('@models/Tag')
|
let Tags = require('@models/Tag')
|
||||||
let Attachment = require('@models/Attachment')
|
let Attachment = require('@models/Attachment')
|
||||||
let ShareNote = require('@models/ShareNote')
|
let ShareNote = require('@models/ShareNote')
|
||||||
@ -8,14 +10,90 @@ let ProcessText = require('@helpers/ProcessText')
|
|||||||
|
|
||||||
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
|
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
|
||||||
|
|
||||||
|
const crypto = require('crypto')
|
||||||
const cs = require('@helpers/CryptoString')
|
const cs = require('@helpers/CryptoString')
|
||||||
const rp = require('request-promise');
|
const rp = require('request-promise');
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
|
||||||
let Note = module.exports = {}
|
|
||||||
|
|
||||||
const gm = require('gm')
|
const gm = require('gm')
|
||||||
|
|
||||||
|
Note.test = (userId, masterKey) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
let testNoteId = 0
|
||||||
|
|
||||||
|
Note.create(userId, '','', masterKey)
|
||||||
|
.then(newNoteId => {
|
||||||
|
|
||||||
|
console.log('Test: Create Note - Pass')
|
||||||
|
testNoteId = newNoteId
|
||||||
|
|
||||||
|
return Note.update
|
||||||
|
(null, userId, testNoteId, 'Note text', 'Test Note beans Title', 0, 0, 0, 'hash', masterKey)
|
||||||
|
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
|
||||||
|
console.log('Test: Update Note - Pass')
|
||||||
|
|
||||||
|
return Note.get(userId, testNoteId, masterKey)
|
||||||
|
|
||||||
|
})
|
||||||
|
.then(updatedText => {
|
||||||
|
|
||||||
|
console.log('Test: Open Updated Note - Pass')
|
||||||
|
|
||||||
|
const shareUserId = 61
|
||||||
|
return ShareNote.migrateNoteToShared(userId, testNoteId, shareUserId, masterKey)
|
||||||
|
})
|
||||||
|
.then(shareResults => {
|
||||||
|
|
||||||
|
console.log('Test: Set Note To Shared - Pass')
|
||||||
|
|
||||||
|
return Note.get(userId, testNoteId, masterKey)
|
||||||
|
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
|
||||||
|
console.log('Test: Open Shared Note - Pass')
|
||||||
|
|
||||||
|
return Note.update
|
||||||
|
(null, userId, testNoteId, 'Shared Update', 'Test Note beans Title', 0, 0, 0, 'hash', masterKey)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
|
||||||
|
console.log('Test: Update Shared Note - Pass')
|
||||||
|
|
||||||
|
return Note.reindex(userId, masterKey)
|
||||||
|
})
|
||||||
|
.then( reindexResults => {
|
||||||
|
|
||||||
|
console.log(`Test: Reindex Notes - ${reindexResults?'Pass':'Fail'}`)
|
||||||
|
|
||||||
|
return Note.encryptedIndexSearch(userId, 'beans', null, masterKey)
|
||||||
|
|
||||||
|
})
|
||||||
|
.then(textSearchResults => {
|
||||||
|
|
||||||
|
if(textSearchResults['ids'] && textSearchResults['ids'].length >= 1){
|
||||||
|
console.log('Test: Search Index - Pass')
|
||||||
|
} else { console.log('Test: Search Index - Fail') }
|
||||||
|
|
||||||
|
return Note.delete(userId, testNoteId)
|
||||||
|
|
||||||
|
})
|
||||||
|
.then(results => {
|
||||||
|
|
||||||
|
console.log('Test: Delete Note - Pass')
|
||||||
|
|
||||||
|
return resolve('Test: Complete')
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
//User doesn't have an encrypted note set. Encrypt all notes
|
//User doesn't have an encrypted note set. Encrypt all notes
|
||||||
Note.encryptEveryNote = (userId, masterKey) => {
|
Note.encryptEveryNote = (userId, masterKey) => {
|
||||||
|
|
||||||
@ -85,6 +163,7 @@ Note.encryptEveryNote = (userId, masterKey) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Returns insertedId of new note
|
||||||
Note.create = (userId, noteTitle, noteText, masterKey) => {
|
Note.create = (userId, noteTitle, noteText, masterKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
@ -92,6 +171,7 @@ Note.create = (userId, noteTitle, noteText, masterKey) => {
|
|||||||
|
|
||||||
const created = Math.round((+new Date)/1000)
|
const created = Math.round((+new Date)/1000)
|
||||||
const salt = cs.createSmallSalt()
|
const salt = cs.createSmallSalt()
|
||||||
|
const snippetSalt = cs.createSmallSalt()
|
||||||
|
|
||||||
const textObject = JSON.stringify([noteTitle, noteText])
|
const textObject = JSON.stringify([noteTitle, noteText])
|
||||||
const encryptedText = cs.encrypt(masterKey, salt, textObject)
|
const encryptedText = cs.encrypt(masterKey, salt, textObject)
|
||||||
@ -103,8 +183,8 @@ Note.create = (userId, noteTitle, noteText, masterKey) => {
|
|||||||
const rawTextId = rows[0].insertId
|
const rawTextId = rows[0].insertId
|
||||||
|
|
||||||
return db.promise()
|
return db.promise()
|
||||||
.query('INSERT INTO note (user_id, note_raw_text_id, created, quick_note) VALUES (?,?,?,?)',
|
.query('INSERT INTO note (user_id, note_raw_text_id, created, quick_note, snippet_salt) VALUES (?,?,?,?,?)',
|
||||||
[userId, rawTextId, created, 0])
|
[userId, rawTextId, created, 0, snippetSalt])
|
||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
// Indexing is done on save
|
// Indexing is done on save
|
||||||
@ -114,6 +194,9 @@ Note.create = (userId, noteTitle, noteText, masterKey) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called when a note is close
|
||||||
|
// Will attempt to reindex all notes that are flagged in database as not indexed
|
||||||
|
// Limit to 100 notes per batch
|
||||||
Note.reindex = (userId, masterKey) => {
|
Note.reindex = (userId, masterKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
@ -148,7 +231,6 @@ Note.reindex = (userId, masterKey) => {
|
|||||||
|
|
||||||
if(rows[0].length == 0){
|
if(rows[0].length == 0){
|
||||||
|
|
||||||
console.log('Creating a new index')
|
|
||||||
//Create search index entry, return an object
|
//Create search index entry, return an object
|
||||||
searchIndexSalt = cs.createSmallSalt()
|
searchIndexSalt = cs.createSmallSalt()
|
||||||
|
|
||||||
@ -266,7 +348,7 @@ Note.reindex = (userId, masterKey) => {
|
|||||||
})
|
})
|
||||||
.then(rawSearchIndex => {
|
.then(rawSearchIndex => {
|
||||||
|
|
||||||
console.log('All notes indexed')
|
// console.log('All notes indexed')
|
||||||
|
|
||||||
const created = Math.round((+new Date)/1000)
|
const created = Math.round((+new Date)/1000)
|
||||||
const jsonSearchIndex = JSON.stringify(searchIndex)
|
const jsonSearchIndex = JSON.stringify(searchIndex)
|
||||||
@ -281,7 +363,7 @@ Note.reindex = (userId, masterKey) => {
|
|||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
|
// console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
|
||||||
resolve(true)
|
resolve(true)
|
||||||
|
|
||||||
})
|
})
|
||||||
@ -337,68 +419,83 @@ Note.reindex = (userId, masterKey) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived, password = '', passwordHint = '', masterKey) => {
|
// Returns updated note text
|
||||||
|
Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived, hash, masterKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
const now = Math.round((+new Date)/1000)
|
const now = Math.round((+new Date)/1000)
|
||||||
|
|
||||||
db.promise()
|
let noteSnippet = ''
|
||||||
.query(`
|
|
||||||
SELECT note_raw_text_id, salt FROM note
|
let User = require('@models/User')
|
||||||
JOIN note_raw_text ON note_raw_text_id = note_raw_text.id
|
let userPrivateKey = null
|
||||||
WHERE note.id = ? AND user_id = ?`, [noteId, userId])
|
User.getPrivateKey(userId, masterKey)
|
||||||
|
.then(privateKey => {
|
||||||
|
|
||||||
|
userPrivateKey = privateKey
|
||||||
|
|
||||||
|
return db.promise()
|
||||||
|
.query(`
|
||||||
|
SELECT note_raw_text_id, salt, snippet_salt, encrypted_share_password_key FROM note
|
||||||
|
JOIN note_raw_text ON note_raw_text_id = note_raw_text.id
|
||||||
|
WHERE note.id = ? AND user_id = ?`, [noteId, userId])
|
||||||
|
|
||||||
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
const textId = rows[0][0]['note_raw_text_id']
|
const textId = rows[0][0]['note_raw_text_id']
|
||||||
let salt = rows[0][0]['salt']
|
let salt = rows[0][0]['salt']
|
||||||
let noteSnippet = ''
|
let snippetSalt = rows[0][0]['snippet_salt']
|
||||||
|
|
||||||
//If a password is set, create a salt
|
//Shared notes use encrypted key - decrypt key then decrypt note
|
||||||
if(password.length > 3 && !salt){
|
const encryptedShareKey = rows[0][0].encrypted_share_password_key
|
||||||
salt = cs.createSalt()
|
if(encryptedShareKey != null){
|
||||||
|
masterKey = crypto.privateDecrypt(userPrivateKey,
|
||||||
|
Buffer.from(encryptedShareKey, 'base64') )
|
||||||
|
}
|
||||||
|
|
||||||
//Save password hint on first encryption
|
let encryptedNoteText = ''
|
||||||
if(passwordHint.length > 0){
|
//Create encrypted snippet
|
||||||
db.promise().query('UPDATE note_raw_text SET password_hint = ? WHERE id = ?', [passwordHint, textId])
|
const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
|
||||||
|
noteSnippet = cs.encrypt(masterKey, snippetSalt, snippet)
|
||||||
|
|
||||||
|
//Encrypt note text
|
||||||
|
const textObject = JSON.stringify([noteTitle, noteText])
|
||||||
|
encryptedNoteText = cs.encrypt(masterKey, salt, textObject)
|
||||||
|
|
||||||
|
//
|
||||||
|
// @TODO - this needs some kind of rate limiting
|
||||||
|
// A note shared with a lot of users could do a ton of updates every save
|
||||||
|
//
|
||||||
|
//Update text snippet for all other shared users
|
||||||
|
db.promise().query('SELECT * FROM note WHERE note_raw_text_id = ? AND id != ?', [textId, noteId])
|
||||||
|
.then((rows, fields) => {
|
||||||
|
for (var i = 0; i < rows[0].length; i++) {
|
||||||
|
const otherNote = rows[0][i]
|
||||||
|
//Re-encrypt for other user
|
||||||
|
const updatedSnippet = cs.encrypt(masterKey, otherNote.snippet_salt, snippet)
|
||||||
|
db.promise().query('UPDATE note SET snippet = ? WHERE id = ?', [updatedSnippet, otherNote.id])
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
//Encrypt note text if proper data is setup
|
|
||||||
if(password.length > 3 && salt.length > 1000){
|
|
||||||
noteText = cs.encrypt(password, salt, noteText)
|
|
||||||
|
|
||||||
//
|
|
||||||
// @TODO - Do note save data if encryption goes wrong, do some validation
|
|
||||||
//
|
|
||||||
} else {
|
|
||||||
|
|
||||||
//Create encrypted snippet
|
|
||||||
const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
|
|
||||||
noteSnippet = cs.encrypt(masterKey, salt, snippet)
|
|
||||||
|
|
||||||
//Encrypt note text
|
|
||||||
const textObject = JSON.stringify([noteTitle, noteText])
|
|
||||||
noteText = cs.encrypt(masterKey, salt, textObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Update Note text
|
//Update Note text
|
||||||
return db.promise()
|
return db.promise()
|
||||||
.query('UPDATE note_raw_text SET text = ?, snippet = ? ,updated = ?, salt = ? WHERE id = ?', [noteText, noteSnippet, now, salt, textId])
|
.query('UPDATE note_raw_text SET text = ?, updated = ? WHERE id = ?', [encryptedNoteText, now, textId])
|
||||||
})
|
})
|
||||||
.then( (rows, fields) => {
|
.then( (rows, fields) => {
|
||||||
|
|
||||||
const encrypted = password.length > 3 ? 1:0
|
|
||||||
|
|
||||||
//Update other note attributes
|
//Update other note attributes
|
||||||
return db.promise()
|
return db.promise()
|
||||||
.query('UPDATE note SET pinned = ?, archived = ?, color = ?, encrypted = ?, indexed = 0 WHERE id = ? AND user_id = ? LIMIT 1',
|
.query('UPDATE note SET pinned = ?, archived = ?, color = ?, snippet = ?, indexed = 0 WHERE id = ? AND user_id = ? LIMIT 1',
|
||||||
[pinned, archived, color, encrypted, noteId, userId])
|
[pinned, archived, color, noteSnippet, noteId, userId])
|
||||||
|
|
||||||
})
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
//Async solr note reindex
|
if(io){
|
||||||
// Note.reindex(userId, noteId)
|
io.to(userId).emit('new_note_text_saved', {noteId, hash})
|
||||||
|
}
|
||||||
|
|
||||||
//Async attachment reindex
|
//Async attachment reindex
|
||||||
Attachment.scanTextForWebsites(io, userId, noteId, noteText)
|
Attachment.scanTextForWebsites(io, userId, noteId, noteText)
|
||||||
@ -565,121 +662,87 @@ Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Note.get = (userId, noteId, password = '', masterKey) => {
|
Note.get = (userId, noteId, masterKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
let User = require('@models/User')
|
||||||
|
|
||||||
if(!masterKey || masterKey.length == 0){
|
if(!masterKey || masterKey.length == 0){
|
||||||
return reject('Get note called without master key')
|
return reject('Get note called without master key')
|
||||||
}
|
}
|
||||||
|
|
||||||
db.promise()
|
let userPrivateKey = null;
|
||||||
.query(`
|
|
||||||
SELECT
|
User.getPrivateKey(userId, masterKey)
|
||||||
note_raw_text.text,
|
.then(privateKey => {
|
||||||
note_raw_text.salt,
|
|
||||||
note_raw_text.password_hint,
|
//Grab users private key
|
||||||
note_raw_text.updated as updated,
|
userPrivateKey = privateKey
|
||||||
note_raw_text.decrypt_attempts_count,
|
|
||||||
note_raw_text.last_decrypted_date,
|
return db.promise()
|
||||||
note.id,
|
.query(`
|
||||||
note.user_id,
|
SELECT
|
||||||
note.created,
|
note_raw_text.text,
|
||||||
note.pinned,
|
note_raw_text.salt,
|
||||||
note.archived,
|
note_raw_text.updated as updated,
|
||||||
note.color,
|
note.id,
|
||||||
note.encrypted,
|
note.user_id,
|
||||||
count(distinct attachment.id) as attachment_count,
|
note.created,
|
||||||
note.note_raw_text_id as rawTextId,
|
note.pinned,
|
||||||
shareUser.username as shareUsername
|
note.archived,
|
||||||
FROM note
|
note.color,
|
||||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
note.encrypted_share_password_key,
|
||||||
LEFT JOIN attachment ON (note.id = attachment.note_id)
|
count(distinct attachment.id) as attachment_count,
|
||||||
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
note.note_raw_text_id as rawTextId,
|
||||||
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId])
|
shareUser.username as shareUsername
|
||||||
|
FROM note
|
||||||
|
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||||
|
LEFT JOIN attachment ON (note.id = attachment.note_id)
|
||||||
|
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
||||||
|
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId])
|
||||||
|
|
||||||
|
})
|
||||||
.then((rows, fields) => {
|
.then((rows, fields) => {
|
||||||
|
|
||||||
const nowTime = Math.round((+new Date)/1000)
|
const nowTime = Math.round((+new Date)/1000)
|
||||||
let noteLockedOut = false
|
let noteLockedOut = false
|
||||||
let noteData = rows[0][0]
|
let noteData = rows[0][0]
|
||||||
const rawTextId = noteData['rawTextId']
|
// const rawTextId = noteData['rawTextId']
|
||||||
noteData.decrypted = true
|
|
||||||
|
|
||||||
//Block access to notes if invalid or user doesn't have access
|
//Block access to notes if invalid or user doesn't have access
|
||||||
if(!noteData || !noteData['user_id'] || noteData['user_id'] != userId || noteData['id'] != noteId){
|
if(!noteData || !noteData['user_id'] || noteData['user_id'] != userId || noteData['id'] != noteId){
|
||||||
return resolve(false)
|
return resolve(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Shared notes use encrypted key - decrypt key then decrypt note
|
||||||
//If this is not and encrypted note, pass decrypted true, skip encryption stuff
|
if(noteData.encrypted_share_password_key != null){
|
||||||
if(noteData.encrypted == 1){
|
masterKey = crypto.privateDecrypt(userPrivateKey,
|
||||||
noteData.decrypted = false
|
Buffer.from(noteData.encrypted_share_password_key, 'base64') )
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//Normal Encrypted note
|
||||||
//Rate Limiting
|
const decipheredText = cs.decrypt(masterKey, noteData.salt, noteData.text)
|
||||||
//
|
if(decipheredText == null){
|
||||||
//Check if note is exceeding decrypt attempt limit
|
throw new Error('Unable to decropt note text')
|
||||||
if(noteData.encrypted == 1){
|
|
||||||
const timeSinceLastUnlock = nowTime - noteData.last_decrypted_date
|
|
||||||
|
|
||||||
//To many attempts in less than 5 minutes, note is locked
|
|
||||||
if(noteData.decrypt_attempts_count > 3 && timeSinceLastUnlock < 300){
|
|
||||||
noteLockedOut = true
|
|
||||||
}
|
|
||||||
|
|
||||||
//its been 5 minutes, reset attempt count
|
|
||||||
if(noteData.decrypt_attempts_count > 0 && timeSinceLastUnlock > 300){
|
|
||||||
noteLockedOut = false
|
|
||||||
noteData.decrypt_attempts_count = 0
|
|
||||||
db.promise().query('UPDATE note_raw_text SET last_decrypted_date = ?, decrypt_attempts_count = 0 WHERE id = ?', [nowTime, rawTextId ])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Note is encrypted, lets try and decipher it with the given password
|
|
||||||
if(password.length > 3 && noteData.encrypted == 1 && !noteLockedOut){
|
|
||||||
|
|
||||||
const decipheredText = cs.decrypt(password, noteData.salt, noteData.text)
|
|
||||||
|
|
||||||
//Text was decrypted, return decrypted text
|
|
||||||
if(decipheredText !== null){
|
|
||||||
noteData.decrypted = true
|
|
||||||
noteData.text = decipheredText
|
|
||||||
|
|
||||||
//Save last decrypted date, reset decrypt atempts
|
|
||||||
db.promise().query('UPDATE note_raw_text SET last_decrypted_date = ?, decrypt_attempts_count = 0 WHERE id = ?', [nowTime, rawTextId ])
|
|
||||||
|
|
||||||
}
|
|
||||||
//Text was not deciphered, delete object, never return cipher text
|
|
||||||
if(decipheredText === null){
|
|
||||||
noteData.text = '' //Never return cipher text
|
|
||||||
noteData.decryptFail = true
|
|
||||||
noteData.decrypt_attempts_count++ //Update display for user
|
|
||||||
|
|
||||||
//Update decrypt attempts
|
|
||||||
db.promise().query('UPDATE note_raw_text SET decrypt_attempts_count = decrypt_attempts_count +1 WHERE id = ?', [rawTextId ])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(noteData.encrypted == 0 && noteData.salt && noteData.salt.length > 0){
|
|
||||||
//Normal Encrypted note
|
|
||||||
const decipheredText = cs.decrypt(masterKey, noteData.salt, noteData.text)
|
|
||||||
if(decipheredText == null){
|
|
||||||
throw new Error('Unable to decropt note text')
|
|
||||||
}
|
|
||||||
//Parse title and text from encrypted data and update object
|
|
||||||
const textObject = JSON.parse(decipheredText)
|
|
||||||
noteData.title = textObject[0]
|
|
||||||
noteData.text = textObject[1]
|
|
||||||
}
|
}
|
||||||
|
//Parse title and text from encrypted data and update object
|
||||||
|
const textObject = JSON.parse(decipheredText)
|
||||||
|
noteData.title = textObject[0]
|
||||||
|
noteData.text = textObject[1]
|
||||||
|
|
||||||
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
|
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
|
||||||
|
|
||||||
//Return note data
|
//Return note data
|
||||||
delete noteData.salt //remove salt from return data
|
delete noteData.salt //remove salt from return data
|
||||||
|
delete noteData.encrypted_share_password_key
|
||||||
noteData.lockedOut = noteLockedOut
|
noteData.lockedOut = noteLockedOut
|
||||||
resolve(noteData)
|
resolve(noteData)
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(console.log)
|
.catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
resolve(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -699,7 +762,7 @@ Note.getShared = (noteId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Searches text index, returns nothing if there is no search query
|
// Searches text index, returns nothing if there is no search query
|
||||||
Note.solrQuery = (userId, searchQuery, searchTags, masterKey) => {
|
Note.encryptedIndexSearch = (userId, searchQuery, searchTags, masterKey) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
if(searchQuery.length == 0){
|
if(searchQuery.length == 0){
|
||||||
@ -752,7 +815,7 @@ Note.solrQuery = (userId, searchQuery, searchTags, masterKey) => {
|
|||||||
searchData['ids'] = searchData['exact'].concat(searchData['partial'])
|
searchData['ids'] = searchData['exact'].concat(searchData['partial'])
|
||||||
searchData['total'] = searchData['ids'].length
|
searchData['total'] = searchData['ids'].length
|
||||||
|
|
||||||
console.log(searchData['total'])
|
// console.log(searchData['total'])
|
||||||
|
|
||||||
return resolve({ 'ids':searchData['ids'] })
|
return resolve({ 'ids':searchData['ids'] })
|
||||||
|
|
||||||
@ -772,10 +835,19 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
//Define return data objects
|
//Define return data objects
|
||||||
let returnData = {
|
let returnData = {
|
||||||
'notes':[],
|
'notes':[],
|
||||||
'tags':[]
|
'total':0,
|
||||||
}
|
}
|
||||||
|
|
||||||
Note.solrQuery(userId, searchQuery, searchTags, masterKey).then( (textSearchResults) => {
|
let userPrivateKey = null
|
||||||
|
|
||||||
|
let User = require('@models/User')
|
||||||
|
User.generateKeypair(userId, masterKey)
|
||||||
|
.then(({publicKey, privateKey}) => {
|
||||||
|
|
||||||
|
userPrivateKey = privateKey
|
||||||
|
return Note.encryptedIndexSearch(userId, searchQuery, searchTags, masterKey)
|
||||||
|
})
|
||||||
|
.then( (textSearchResults) => {
|
||||||
|
|
||||||
//Pull out search results from previous query
|
//Pull out search results from previous query
|
||||||
let textSearchIds = []
|
let textSearchIds = []
|
||||||
@ -784,6 +856,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
|
|
||||||
if(textSearchResults != null){
|
if(textSearchResults != null){
|
||||||
textSearchIds = textSearchResults['ids']
|
textSearchIds = textSearchResults['ids']
|
||||||
|
returnData['total'] = textSearchIds.length
|
||||||
// highlights = textSearchResults['snippets']
|
// highlights = textSearchResults['snippets']
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -799,9 +872,8 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
let searchParams = [userId]
|
let searchParams = [userId]
|
||||||
let noteSearchQuery = `
|
let noteSearchQuery = `
|
||||||
SELECT note.id,
|
SELECT note.id,
|
||||||
note_raw_text.title as title,
|
note.snippet as snippet,
|
||||||
note_raw_text.snippet as snippet,
|
note.snippet_salt as salt,
|
||||||
note_raw_text.salt as salt,
|
|
||||||
note_raw_text.updated as updated,
|
note_raw_text.updated as updated,
|
||||||
opened,
|
opened,
|
||||||
color,
|
color,
|
||||||
@ -813,7 +885,8 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
GROUP_CONCAT(DISTINCT tag.text) as tags,
|
GROUP_CONCAT(DISTINCT tag.text) as tags,
|
||||||
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
|
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
|
||||||
shareUser.username as shareUsername,
|
shareUser.username as shareUsername,
|
||||||
note.shared
|
note.shared,
|
||||||
|
note.encrypted_share_password_key
|
||||||
FROM note
|
FROM note
|
||||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||||
LEFT JOIN note_tag ON (note.id = note_tag.note_id)
|
LEFT JOIN note_tag ON (note.id = note_tag.note_id)
|
||||||
@ -924,6 +997,9 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
.query(noteSearchQuery, searchParams)
|
.query(noteSearchQuery, searchParams)
|
||||||
.then((noteRows, noteFields) => {
|
.then((noteRows, noteFields) => {
|
||||||
|
|
||||||
|
//Current note key may change, default to master key
|
||||||
|
let currentNoteKey = masterKey
|
||||||
|
|
||||||
//Push all notes
|
//Push all notes
|
||||||
returnData['notes'] = noteRows[0]
|
returnData['notes'] = noteRows[0]
|
||||||
|
|
||||||
@ -934,12 +1010,18 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
//Grab note ID for finding tags
|
//Grab note ID for finding tags
|
||||||
noteIds.push(note.id)
|
noteIds.push(note.id)
|
||||||
|
|
||||||
if(note.encrypted == 1){
|
|
||||||
note.text = ''
|
//Shared notes use encrypted key - decrypt key then decrypt note
|
||||||
|
const encryptedShareKey = note.encrypted_share_password_key
|
||||||
|
if(encryptedShareKey != null){
|
||||||
|
currentNoteKey = crypto.privateDecrypt(userPrivateKey,
|
||||||
|
Buffer.from(encryptedShareKey, 'base64') )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//Decrypt note text
|
//Decrypt note text
|
||||||
if(note.snippet && note.salt){
|
if(note.snippet && note.salt){
|
||||||
const decipheredText = cs.decrypt(masterKey, note.salt, note.snippet)
|
const decipheredText = cs.decrypt(currentNoteKey, note.salt, note.snippet)
|
||||||
const textObject = JSON.parse(decipheredText)
|
const textObject = JSON.parse(decipheredText)
|
||||||
if(textObject != null && textObject.length == 2){
|
if(textObject != null && textObject.length == 2){
|
||||||
note.title = textObject[0]
|
note.title = textObject[0]
|
||||||
@ -953,6 +1035,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
note.title = textData.title
|
note.title = textData.title
|
||||||
note.subtext = textData.sub
|
note.subtext = textData.sub
|
||||||
|
|
||||||
|
//Remove these variables
|
||||||
note.note_highlights = []
|
note.note_highlights = []
|
||||||
note.attachment_highlights = []
|
note.attachment_highlights = []
|
||||||
note.tag_highlights = []
|
note.tag_highlights = []
|
||||||
@ -967,38 +1050,13 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Clear out note.text before sending it to front end, its being used in title and subtext
|
//Clear out note.text before sending it to front end, its being used in title and subtext
|
||||||
delete note.text
|
|
||||||
delete note.snippet
|
delete note.snippet
|
||||||
delete note.salt
|
delete note.salt
|
||||||
})
|
})
|
||||||
|
|
||||||
//If no notes are returned, there are no tags, return empty
|
|
||||||
if(noteIds.length == 0){
|
|
||||||
return resolve(returnData)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return all notes, tags are not being searched
|
return resolve(returnData)
|
||||||
// if tags are being searched, continue
|
|
||||||
// if notes are being filtered, return tags
|
|
||||||
if(searchTags.length == 0 && returnTagResults == false){
|
|
||||||
return resolve(returnData)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Only show tags of selected notes
|
|
||||||
db.promise()
|
|
||||||
.query(`SELECT tag.id, tag.text, count(tag.id) as usages FROM note_tag
|
|
||||||
JOIN tag ON (tag.id = note_tag.tag_id)
|
|
||||||
WHERE note_tag.user_id = ?
|
|
||||||
AND note_id IN (?)
|
|
||||||
GROUP BY tag.id
|
|
||||||
ORDER BY usages DESC;`,[userId, noteIds])
|
|
||||||
.then((tagRows, tagFields) => {
|
|
||||||
|
|
||||||
returnData['tags'] = tagRows[0]
|
|
||||||
|
|
||||||
resolve(returnData)
|
|
||||||
})
|
|
||||||
.catch(console.log)
|
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
@ -9,80 +9,122 @@ 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 = ? 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) => {
|
||||||
|
|
||||||
|
let success = true
|
||||||
|
return resolve({success, shareUserId})
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
console.log('Shared Note Error')
|
||||||
console.log(error)
|
console.log(error)
|
||||||
resolve(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
var crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
|
|
||||||
const Note = require('@models/Note')
|
const Note = require('@models/Note')
|
||||||
|
|
||||||
@ -30,9 +30,10 @@ User.login = (username, password) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if(lookedUpUser && lookedUpUser.salt){
|
if(rows[0].length == 1){
|
||||||
//hash the password and check for a match
|
//hash the password and check for a match
|
||||||
const salt = new Buffer(lookedUpUser.salt, 'binary')
|
// 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){
|
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
|
||||||
if(delivered_key.toString('hex') === lookedUpUser.password){
|
if(delivered_key.toString('hex') === lookedUpUser.password){
|
||||||
|
|
||||||
@ -40,9 +41,14 @@ User.login = (username, password) => {
|
|||||||
.then( result => User.getMasterKey(lookedUpUser.id, password))
|
.then( result => User.getMasterKey(lookedUpUser.id, password))
|
||||||
.then(masterKey => {
|
.then(masterKey => {
|
||||||
|
|
||||||
//Passback a json web token
|
User.generateKeypair(lookedUpUser.id, masterKey)
|
||||||
const token = Auth.createToken(lookedUpUser.id, masterKey)
|
.then(({publicKey, privateKey}) => {
|
||||||
resolve({ token: token, userId:lookedUpUser.id })
|
|
||||||
|
//Passback a json web token
|
||||||
|
const token = Auth.createToken(lookedUpUser.id, masterKey)
|
||||||
|
resolve({ token: token, userId:lookedUpUser.id })
|
||||||
|
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -80,7 +86,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) {
|
||||||
@ -108,8 +114,14 @@ User.create = (username, password) => {
|
|||||||
.then( result => User.getMasterKey(userId, password))
|
.then( result => User.getMasterKey(userId, password))
|
||||||
.then(masterKey => {
|
.then(masterKey => {
|
||||||
|
|
||||||
const token = Auth.createToken(userId, masterKey)
|
User.generateKeypair(userId, masterKey)
|
||||||
return resolve({token, userId})
|
.then(({publicKey, privateKey}) => {
|
||||||
|
|
||||||
|
const token = Auth.createToken(userId, masterKey)
|
||||||
|
return resolve({token, userId})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -202,7 +214,6 @@ User.generateMasterKey = (userId, password) => {
|
|||||||
} else {
|
} else {
|
||||||
// Generate user key, its big and random
|
// Generate user key, its big and random
|
||||||
const masterPassword = cs.createSmallSalt()
|
const masterPassword = cs.createSmallSalt()
|
||||||
console.log('Generating new key for user', userId)
|
|
||||||
|
|
||||||
//Generate a salt because it wants it
|
//Generate a salt because it wants it
|
||||||
const salt = cs.createSmallSalt()
|
const salt = cs.createSmallSalt()
|
||||||
@ -262,3 +273,142 @@ User.getMasterKey = (userId, password) => {
|
|||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
//Delete user, all notes, all keys
|
||||||
|
}
|
||||||
|
|
||||||
|
User.keyPairTest = (testUserName = 'genMan', password = '1') => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
let masterKey = null
|
||||||
|
let testUserId = null
|
||||||
|
|
||||||
|
User.login(testUserName, password)
|
||||||
|
.then( ({ token, userId }) => {
|
||||||
|
testUserId = userId
|
||||||
|
console.log('Test: Create/Login User - Pass')
|
||||||
|
return User.getMasterKey(testUserId, password)
|
||||||
|
})
|
||||||
|
.then(newMasterKey => {
|
||||||
|
masterKey = newMasterKey
|
||||||
|
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
|
||||||
|
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
|
||||||
|
console.log(publicDeccryptMessage.toString('utf8'))
|
||||||
|
|
||||||
|
resolve({testUserId, masterKey})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
var express = require('express')
|
var express = require('express')
|
||||||
var router = express.Router()
|
var router = express.Router()
|
||||||
|
|
||||||
let Notes = require('@models/Note');
|
let Notes = 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 masterKey = null
|
let masterKey = null
|
||||||
@ -21,7 +22,7 @@ router.use(function setUserId (req, res, next) {
|
|||||||
// Note actions
|
// Note actions
|
||||||
//
|
//
|
||||||
router.post('/get', function (req, res) {
|
router.post('/get', function (req, res) {
|
||||||
Notes.get(userId, req.body.noteId, req.body.password, masterKey)
|
Notes.get(userId, req.body.noteId, masterKey)
|
||||||
.then( data => {
|
.then( data => {
|
||||||
res.send(data)
|
res.send(data)
|
||||||
})
|
})
|
||||||
@ -38,7 +39,7 @@ router.post('/create', function (req, res) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
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, masterKey)
|
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.hash, masterKey)
|
||||||
.then( id => res.send({id}) )
|
.then( id => res.send({id}) )
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -90,7 +91,11 @@ 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
|
//Emit update count event to user shared with - so they see the note in real time
|
||||||
|
Loading…
Reference in New Issue
Block a user