|
|
|
@@ -41,12 +41,69 @@
|
|
|
|
|
<!-- Squire box grows -->
|
|
|
|
|
<div class="note-wrapper">
|
|
|
|
|
|
|
|
|
|
<div id="squire-id" class="squire-box" ref="squirebox"></div>
|
|
|
|
|
<textarea
|
|
|
|
|
ref="titleTextarea"
|
|
|
|
|
v-on:keyup="titleResize"
|
|
|
|
|
v-on:keydown="titleResize"
|
|
|
|
|
@keydown.enter.exact.prevent="editor.focus()"
|
|
|
|
|
rows="1"
|
|
|
|
|
:style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}"
|
|
|
|
|
v-on:blur="save" type="text" v-model="noteTitle" placeholder="Title" class="stealth-input">
|
|
|
|
|
</textarea>
|
|
|
|
|
|
|
|
|
|
<div v-show="isDecrypted" 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" :data-tooltip="`Unlock Attempts: ${decryptAttempts}`">
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- bottom stats -->
|
|
|
|
|
<div class="ui basic segment">
|
|
|
|
|
<div class="ui grid">
|
|
|
|
|
<div class="sixteen wide column">
|
|
|
|
|
|
|
|
|
|
<div class="ui basic button"v-if="!isEncrypted" v-on:click="passwordEnterVisible = true">
|
|
|
|
|
<i class="shield alternate icon"></i>
|
|
|
|
|
Password Protect
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ui icon basic button" v-if="isEncrypted && isDecrypted" v-on:click="disableEncryption">
|
|
|
|
|
<i class="unlock icon"></i>
|
|
|
|
|
Remove Password
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="ui basic compact button">
|
|
|
|
|
Status: {{ statusText }}
|
|
|
|
|
</div>
|
|
|
|
@@ -56,6 +113,8 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- && this.$store.getters.getIsUserOnMobile -->
|
|
|
|
@@ -202,7 +261,56 @@
|
|
|
|
|
</div>
|
|
|
|
|
</side-slide-menu>
|
|
|
|
|
|
|
|
|
|
<div class="full-focus-shade"></div>
|
|
|
|
|
<side-slide-menu v-if="passwordEnterVisible" v-on:close="passwordEnterVisible = false" :fullShadow="true" name="encrypt note">
|
|
|
|
|
<div class="ui basic segment" v-if="isDecrypted && isEncrypted">
|
|
|
|
|
<p>Note Decrypted</p>
|
|
|
|
|
<div class="ui green button" v-on:click="lockNote">Lock Note</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="!isEncrypted" class="ui basic segment">
|
|
|
|
|
|
|
|
|
|
<div class="ui top attached segment">
|
|
|
|
|
|
|
|
|
|
<h2><i class="green lock alternate icon"></i>Password protect this Note</h2>
|
|
|
|
|
<p>Password protection will prevent anyone from reading the text of this note, unless they enter the correct password.</p>
|
|
|
|
|
<p><b>Only the note text is protected. Title, tags, and files are not encrypted and remain visible without a password.</b></p>
|
|
|
|
|
<p>The password you select will only be used for this note. You can use the same password on multiple notes. The note will be encrypted using the password entered. A longer password is will be more secure.</p>
|
|
|
|
|
<h4><i class="red icon exclamation triangle"></i> Warning. There is no way to recover a lost password.</h4>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="ui bottom attached segment">
|
|
|
|
|
<div class="ui form">
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>New Password to lock this note</label>
|
|
|
|
|
<input :name="`randomThing-${noteid}`" :id="`yupper-${noteid}`"type="password" v-model="password" placeholder="Note Password">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field" v-if="password.length > 3">
|
|
|
|
|
<label>Confirm Password</label>
|
|
|
|
|
<input :name="`randomStuff-${noteid}`" :id="`heckye-${noteid}`"type="password" v-model="passwordConfirm" placeholder="Confirm Password">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field" v-if="password.length > 3">
|
|
|
|
|
<label>Password Hint - visible when unlocking note</label>
|
|
|
|
|
<input :name="`randomStuzz-${noteid}`" :id="`heckyo-${noteid}`"type="text" v-model="passwordHint" placeholder="Optional Password Hint" v-on:keyup.enter="enableEncryption">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="field" v-if="passwordConfirm.length > 3 && password != passwordConfirm">
|
|
|
|
|
<div v-on:click="enableEncryption" class="ui disabled green button">
|
|
|
|
|
Passwords do not match
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field" v-if="passwordConfirm.length > 3 && password == passwordConfirm">
|
|
|
|
|
<div v-on:click="enableEncryption" class="ui green button">
|
|
|
|
|
Protect!
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</side-slide-menu>
|
|
|
|
|
|
|
|
|
|
<!-- <div class="full-focus-shade"></div> -->
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
@@ -210,6 +318,7 @@
|
|
|
|
|
<script>
|
|
|
|
|
|
|
|
|
|
import axios from 'axios'
|
|
|
|
|
const crypto = require('crypto')
|
|
|
|
|
const DiffMatchPatch = require('../../../server/helpers/DiffMatchPatch')
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
@@ -233,6 +342,7 @@
|
|
|
|
|
currentNoteId: 0,
|
|
|
|
|
modified: false,
|
|
|
|
|
noteText: '',
|
|
|
|
|
noteTitle: '',
|
|
|
|
|
rawTextId: 0,
|
|
|
|
|
created: '',
|
|
|
|
|
updated: '',
|
|
|
|
@@ -270,6 +380,18 @@
|
|
|
|
|
colorPickerVisible: false,
|
|
|
|
|
showFilesSideMenu: false,
|
|
|
|
|
showNoteOptions: 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,
|
|
|
|
|
passwordEnterVisible: false,
|
|
|
|
|
decryptAttempts: 0,
|
|
|
|
|
lockedOut: false,
|
|
|
|
|
autoLockTimeout: null,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
@@ -299,6 +421,11 @@
|
|
|
|
|
},
|
|
|
|
|
beforeDestroy(){
|
|
|
|
|
|
|
|
|
|
this.password = ''
|
|
|
|
|
this.passwordConfirm = ''
|
|
|
|
|
this.hashedPass = ''
|
|
|
|
|
clearTimeout(this.autoLockTimeout)
|
|
|
|
|
|
|
|
|
|
this.$io.emit('leave_room', this.rawTextId)
|
|
|
|
|
|
|
|
|
|
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
|
|
|
|
@@ -326,7 +453,14 @@
|
|
|
|
|
|
|
|
|
|
//focus on open, not on mobile, thats annoying
|
|
|
|
|
if(!this.$store.getters.getIsUserOnMobile){
|
|
|
|
|
this.editor.focus()
|
|
|
|
|
// this.editor.focus()
|
|
|
|
|
|
|
|
|
|
if(this.noteTitle && this.noteTitle.length == 0){
|
|
|
|
|
this.$refs.titleTextarea.focus()
|
|
|
|
|
} else {
|
|
|
|
|
this.editor.moveCursorToEnd()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Click Event - Open links when clicked in editor or toggle checks
|
|
|
|
@@ -705,48 +839,68 @@
|
|
|
|
|
},
|
|
|
|
|
loadNote(noteId){
|
|
|
|
|
|
|
|
|
|
let vm = this
|
|
|
|
|
|
|
|
|
|
//Generate a random loading message
|
|
|
|
|
let doing = ['Loading','Loading','Getting','Fetching','Grabbing','Sequencing','Organizing','Untangling','Processing','Refining','Extracting','Fusing','Pruning','Expanding','Enlarging','Transfiguring','Quantizing','Ingratiating','Lumping']
|
|
|
|
|
let thing = ['Note','Note','Note','Note','Data','Text','Document','Algorithm','Buffer','Client','Download','File','Frame','Graphics','Hardware','HTML','Interface','Logic','Mainframe','Memory','Media','Nodes','Network','Chaos']
|
|
|
|
|
let p1 = doing[Math.floor(Math.random() * doing.length)]
|
|
|
|
|
let p2 = thing[Math.floor(Math.random() * thing.length)]
|
|
|
|
|
vm.loadingMessage = p1 + ' ' + p2
|
|
|
|
|
this.loadingMessage = p1 + ' ' + p2
|
|
|
|
|
|
|
|
|
|
//Component is activated with NoteId in place, lookup text with associated ID
|
|
|
|
|
if(this.$store.getters.getLoggedIn){
|
|
|
|
|
axios.post('/api/note/get', {'noteId': noteId})
|
|
|
|
|
axios.post('/api/note/get', { 'noteId': this.noteid, 'password':this.hashedPass })
|
|
|
|
|
.then(response => {
|
|
|
|
|
|
|
|
|
|
//Set up local data
|
|
|
|
|
vm.currentNoteId = noteId
|
|
|
|
|
this.currentNoteId = this.noteid
|
|
|
|
|
this.rawTextId = response.data.rawTextId
|
|
|
|
|
this.shareUsername = response.data.shareUsername
|
|
|
|
|
this.passwordHint = response.data.password_hint
|
|
|
|
|
|
|
|
|
|
this.created = response.data.created
|
|
|
|
|
this.updated = response.data.updated
|
|
|
|
|
this.noteTitle = response.data.title
|
|
|
|
|
|
|
|
|
|
vm.noteText = response.data.text
|
|
|
|
|
vm.diffNoteText = response.data.text
|
|
|
|
|
this.noteText = response.data.text
|
|
|
|
|
this.diffNoteText = response.data.text
|
|
|
|
|
|
|
|
|
|
vm.lastNoteHash = vm.hashString(response.data.text)
|
|
|
|
|
this.lastNoteHash = this.hashString(response.data.text)
|
|
|
|
|
//Set up note colors
|
|
|
|
|
if(response.data.color){
|
|
|
|
|
vm.styleObject = JSON.parse(response.data.color)
|
|
|
|
|
this.styleObject = JSON.parse(response.data.color)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(response.data.pinned != null){
|
|
|
|
|
vm.pinned = response.data.pinned
|
|
|
|
|
this.pinned = response.data.pinned
|
|
|
|
|
}
|
|
|
|
|
vm.archived = response.data.archived
|
|
|
|
|
vm.attachmentCount = response.data.attachment_count
|
|
|
|
|
this.archived = response.data.archived
|
|
|
|
|
this.attachmentCount = response.data.attachment_count
|
|
|
|
|
|
|
|
|
|
this.loading = false
|
|
|
|
|
|
|
|
|
|
vm.$nextTick(() => {
|
|
|
|
|
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(() => {
|
|
|
|
|
//Adjust note title size after load
|
|
|
|
|
this.titleResize()
|
|
|
|
|
|
|
|
|
|
this.setupWebSockets()
|
|
|
|
|
this.initSquire()
|
|
|
|
|
this.startAutolockTimer()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
@@ -917,7 +1071,7 @@
|
|
|
|
|
.createExpression(xpath)
|
|
|
|
|
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE) .singleNodeValue
|
|
|
|
|
},
|
|
|
|
|
onKeyup(event){
|
|
|
|
|
onKeyup(){
|
|
|
|
|
|
|
|
|
|
this.statusText = 'Save'
|
|
|
|
|
|
|
|
|
@@ -943,6 +1097,12 @@
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
const currentNoteText = this.getText()
|
|
|
|
|
if( this.lastNoteHash == this.hashString( currentNoteText )){
|
|
|
|
@@ -958,11 +1118,14 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const postData = {
|
|
|
|
|
'noteId':this.currentNoteId,
|
|
|
|
|
'noteId': this.currentNoteId,
|
|
|
|
|
'title': this.noteTitle,
|
|
|
|
|
'text': currentNoteText,
|
|
|
|
|
'color': JSON.stringify(this.styleObject), //Save little json color object
|
|
|
|
|
'pinned': this.pinned,
|
|
|
|
|
'archived':this.archived,
|
|
|
|
|
'archived': this.archived,
|
|
|
|
|
'password': this.hashedPass,
|
|
|
|
|
'hint': this.passwordHint,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.statusText = 'Saving'
|
|
|
|
@@ -971,8 +1134,11 @@
|
|
|
|
|
this.updated = Math.round((+new Date)/1000)
|
|
|
|
|
this.modified = true
|
|
|
|
|
|
|
|
|
|
console.log('Saved')
|
|
|
|
|
|
|
|
|
|
//Update last saved note hash
|
|
|
|
|
this.lastNoteHash = this.hashString( currentNoteText )
|
|
|
|
|
this.startAutolockTimer()
|
|
|
|
|
return resolve(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
@@ -1010,6 +1176,8 @@
|
|
|
|
|
},
|
|
|
|
|
hashString(text){
|
|
|
|
|
|
|
|
|
|
text = this.noteTitle + text
|
|
|
|
|
|
|
|
|
|
var hash = 0;
|
|
|
|
|
if (text == null || text.length == 0) {
|
|
|
|
|
return hash;
|
|
|
|
@@ -1055,7 +1223,80 @@
|
|
|
|
|
this.$io.on('incoming_diff', 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.passwordEnterVisible = 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.passwordEnterVisible = 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.passwordEnterVisible = false
|
|
|
|
|
|
|
|
|
|
//Reload Note
|
|
|
|
|
this.save()
|
|
|
|
|
.then(results => {
|
|
|
|
|
this.loadNote()
|
|
|
|
|
this.$bus.$emit('notification', 'Password Protection Removed')
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
titleResize(){
|
|
|
|
|
//Resize the title field
|
|
|
|
|
let element = this.$refs.titleTextarea
|
|
|
|
|
let padding = 0
|
|
|
|
|
|
|
|
|
|
element.style.height = 'auto';
|
|
|
|
|
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>
|
|
|
|
@@ -1090,7 +1331,18 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stealth-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 10px 15px 5px;
|
|
|
|
|
background-color: rgba(255,255,255,0.1);
|
|
|
|
|
border: none;
|
|
|
|
|
font-size: 1.7em;
|
|
|
|
|
/*line-height: 1.7em;*/
|
|
|
|
|
color: var(--text_color);
|
|
|
|
|
resize: none;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*Settings manager styles */
|
|
|
|
|
.all-settings {
|
|
|
|
|