Compare commits
2 Commits
d349fb8328
...
e4fae23623
Author | SHA1 | Date | |
---|---|---|---|
|
e4fae23623 | ||
|
56d4664d0d |
@ -23,22 +23,76 @@ export default {
|
||||
data: function(){
|
||||
return {
|
||||
// loggedIn:
|
||||
fetchingInProgress: false, //Prevent start getting token while fetch is in progress
|
||||
blockUntilNextRequest: false //If token was just renewed, don't fetch more until next request
|
||||
}
|
||||
},
|
||||
|
||||
//Axios response interceptor
|
||||
// - Gets new session tokens from server and uses them in app
|
||||
beforeCreate: function(){
|
||||
|
||||
//Before all requests going out
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
|
||||
//Enable token fetching after another request is made
|
||||
if(this.blockUntilNextRequest){
|
||||
this.fetchingInProgress = false
|
||||
this.blockUntilNextRequest = false
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Add a response interceptor, token can be renewed on every response
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
|
||||
if(typeof response.headers.remaininguses !== 'undefined'){
|
||||
|
||||
// console.log(response.headers.remaininguses)
|
||||
//Look at remaining uses of token, if its less than five, request a new one
|
||||
if(response.headers.remaininguses < 10 && !this.fetchingInProgress && !this.blockUntilNextRequest){
|
||||
this.fetchingInProgress = true
|
||||
const currentToken = localStorage.getItem('loginToken')
|
||||
this.$io.emit('renew_session_token', currentToken)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
//Puts token into state on page load
|
||||
let token = localStorage.getItem('loginToken')
|
||||
let username = localStorage.getItem('username')
|
||||
|
||||
// const socket = io({ path:'/socket' });
|
||||
const socket = this.$io
|
||||
socket.on('connect', () => {
|
||||
//
|
||||
if(token && token.length > 0){
|
||||
|
||||
this.$store.commit('setSocketIoSocket', socket.id)
|
||||
//setup username display
|
||||
this.$store.commit('setUsername', username)
|
||||
|
||||
this.$io.emit('user_connect', token)
|
||||
})
|
||||
//Set session token on every request if set
|
||||
axios.defaults.headers.common['authorizationtoken'] = token
|
||||
|
||||
//Setup websockets into vue instance
|
||||
const socket = this.$io
|
||||
socket.on('connect', () => {
|
||||
|
||||
//Put user into personal event room for live note updates, etc
|
||||
this.$io.emit('user_connect', token)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//Detect if user is on a mobile browser and set a flag in store
|
||||
this.$store.commit('detectIsUserOnMobile')
|
||||
@ -49,11 +103,6 @@ export default {
|
||||
this.$store.commit('toggleNightMode', themeNumber)
|
||||
}
|
||||
|
||||
//Put user data into global store on load
|
||||
if(token){
|
||||
this.$store.commit('setLoginToken', {token, username})
|
||||
}
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
@ -63,6 +112,17 @@ export default {
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
})
|
||||
|
||||
this.$io.on('recievend_new_token', newToken => {
|
||||
|
||||
// console.log('Got a new token')
|
||||
|
||||
axios.defaults.headers.common['authorizationtoken'] = newToken
|
||||
localStorage.setItem('loginToken', newToken)
|
||||
|
||||
//Disable getting new tokens until next request
|
||||
this.blockUntilNextRequest = true
|
||||
})
|
||||
|
||||
},
|
||||
computed: {
|
||||
loggedIn () {
|
||||
|
@ -17,6 +17,10 @@
|
||||
|
||||
|
||||
:root {
|
||||
|
||||
--main-accent: #16ab39;
|
||||
|
||||
/*theme colors */
|
||||
--body_bg_color: #f5f6f7;
|
||||
--small_element_bg_color: #fff;
|
||||
--text_color: #3d3d3d;
|
||||
|
@ -248,7 +248,7 @@
|
||||
|
||||
|
||||
|
||||
<div v-on:click="reloadPage" class="version-display">
|
||||
<div v-on:click="reloadPage" class="version-display" v-if="version != 0" >
|
||||
<i :class="`${getVersionIcon()} icon`"></i> {{ version }}
|
||||
</div>
|
||||
|
||||
@ -267,7 +267,7 @@
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
version: '2.3.4',
|
||||
version: '0',
|
||||
username: '',
|
||||
collapsed: false,
|
||||
mobile: false,
|
||||
@ -277,6 +277,7 @@
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
this.mobile = this.$store.getters.getIsUserOnMobile
|
||||
@ -288,6 +289,7 @@
|
||||
|
||||
if(this.loggedIn){
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
this.version = localStorage.getItem('currentVersion')
|
||||
}
|
||||
|
||||
},
|
||||
@ -347,9 +349,12 @@
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') })
|
||||
},
|
||||
destroyLoginToken() {
|
||||
this.$bus.$emit('notification', 'Logged Out')
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$router.push('/')
|
||||
axios.post('/api/user/logout')
|
||||
setTimeout(() => {
|
||||
this.$bus.$emit('notification', 'Logged Out')
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$router.push('/')
|
||||
}, 200)
|
||||
},
|
||||
toggleNightMode(){
|
||||
this.$store.commit('toggleNightMode')
|
||||
|
@ -98,13 +98,15 @@
|
||||
//Login user if we have a valid token
|
||||
if(data && data.token && data.token.length > 0){
|
||||
|
||||
const token = data.token
|
||||
const username = this.username
|
||||
//Set username to local session
|
||||
this.$store.commit('setUsername', this.username)
|
||||
|
||||
this.$store.commit('setLoginToken', {token, username})
|
||||
const token = data.token
|
||||
|
||||
//Setup socket io after user logs in
|
||||
axios.defaults.headers.common['authorizationtoken'] = token
|
||||
this.$io.emit('user_connect', token)
|
||||
localStorage.setItem('loginToken', token)
|
||||
|
||||
//Redirect user to notes section after login
|
||||
this.$router.push('/notes')
|
||||
@ -113,7 +115,7 @@
|
||||
register(){
|
||||
|
||||
if( this.username.length == 0 || this.password.length == 0 ){
|
||||
this.$bus.$emit('notification', 'Username and Password Required')
|
||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username and Password Required')
|
||||
return
|
||||
}
|
||||
|
||||
@ -121,19 +123,19 @@
|
||||
.then(({data}) => {
|
||||
|
||||
if(data == false){
|
||||
this.$bus.$emit('notification', 'Username already in use')
|
||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username already in use')
|
||||
}
|
||||
|
||||
this.finalizeLogin(data)
|
||||
})
|
||||
.catch(error => {
|
||||
this.$bus.$emit('notification', 'Username already in use')
|
||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username already in use')
|
||||
})
|
||||
},
|
||||
login(){
|
||||
|
||||
if( this.username.length == 0 || this.password.length == 0 ){
|
||||
this.$bus.$emit('notification', 'Username and Password Required')
|
||||
this.$bus.$emit('notification', 'Unable to Login - Username and Password Required')
|
||||
return
|
||||
}
|
||||
|
||||
@ -141,13 +143,13 @@
|
||||
.then(({data}) => {
|
||||
|
||||
if(data == false){
|
||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||
this.$bus.$emit('notification', 'Unable to Login - Incorrect Username or Password')
|
||||
}
|
||||
|
||||
this.finalizeLogin(data)
|
||||
})
|
||||
.catch(error => {
|
||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||
this.$bus.$emit('notification', 'Unable to Login - Incorrect Username or Password')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -164,13 +164,26 @@
|
||||
|
||||
<!-- Squire Box -->
|
||||
<div id="squire-id" class="squire-box" ref="squirebox" placeholder="Note Text"></div>
|
||||
|
||||
<!-- <div v-if="caretShow" class="artificial-caret" :style="{ 'top':caretTop+'px', 'left':caretLeft+'px' }"></div> -->
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- little tags on the side -->
|
||||
<div class="note-mini-tag-area" :class="{ 'size-down':sizeDown }">
|
||||
<span v-for="tag in allTags" class="subtle-tag active-mini-tag" v-if="isTagOnNote(tag.id)" v-on:click="removeTag(tag.id)">
|
||||
<i class="tag icon"></i>
|
||||
{{ tag.text }}
|
||||
</span>
|
||||
<span v-else class="subtle-tag" v-on:click="addTag(tag.text)">
|
||||
<i class="plus icon"></i>
|
||||
{{ tag.text }}
|
||||
</span>
|
||||
<span class="subtle-tag" v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)">
|
||||
<i class="plus icon"></i><i class="green tags icon"></i>Add Tag
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- color picker -->
|
||||
<color-tooltip
|
||||
v-if="colorpicker"
|
||||
@ -187,7 +200,7 @@
|
||||
/>
|
||||
</side-slide-menu>
|
||||
|
||||
<side-slide-menu v-if="tags" v-on:close="tags = false" name="tags" :style-object="styleObject">
|
||||
<side-slide-menu v-if="tags" v-on:close="tags = false; fetchNoteTags()" name="tags" :style-object="styleObject">
|
||||
<div class="ui basic segment">
|
||||
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
|
||||
</div>
|
||||
@ -246,10 +259,10 @@
|
||||
|
||||
<!-- Show side shades if user is on desktop only -->
|
||||
<div class="full-focus-shade shade1"
|
||||
:class="{ 'slide-out-left':(sizeDown == true) }"
|
||||
:class="{ 'slide-out-left':sizeDown }"
|
||||
v-on:click="close()"></div>
|
||||
<div class="full-focus-shade shade2"
|
||||
:class="{ 'slide-out-right':(sizeDown == true) }"
|
||||
:class="{ 'slide-out-right':sizeDown }"
|
||||
v-on:click="close()"></div>
|
||||
|
||||
</div>
|
||||
@ -327,13 +340,13 @@
|
||||
diffTextTimeout: null,
|
||||
diffsApplied: null,
|
||||
|
||||
//Fake Caret position and visibility
|
||||
caretShow: false,
|
||||
caretLeft: null,
|
||||
caretTop: null,
|
||||
//Used to restore caret position
|
||||
lastRange: null,
|
||||
startOffset: 0,
|
||||
|
||||
//Tag Display
|
||||
allTags: [],
|
||||
noteTags: [],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -449,6 +462,77 @@
|
||||
|
||||
}, totalTime + 40)
|
||||
},
|
||||
removeTag(tagId){
|
||||
|
||||
this.allTags = []
|
||||
let entryId = 0
|
||||
|
||||
//Find fucking note tag for removal
|
||||
this.noteTags.forEach(noteTag => {
|
||||
if(noteTag['tagId'] == tagId){
|
||||
entryId = noteTag['entryId']
|
||||
}
|
||||
})
|
||||
|
||||
let postData = {
|
||||
'tagId':entryId,
|
||||
'noteId':this.noteid
|
||||
}
|
||||
|
||||
axios.post('/api/tag/removefromnote', postData)
|
||||
.then(response => {
|
||||
this.fetchNoteTags()
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Remove Tag') })
|
||||
},
|
||||
addTag(tagText){
|
||||
|
||||
this.allTags = []
|
||||
|
||||
let postData = {
|
||||
'tagText':tagText,
|
||||
'noteId':this.noteid
|
||||
}
|
||||
|
||||
axios.post('/api/tag/addtonote', postData)
|
||||
.then(response => {
|
||||
this.fetchNoteTags()
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Add Tag') })
|
||||
},
|
||||
fetchNoteTags(){
|
||||
axios.post('/api/tag/get', {'noteId': this.noteid})
|
||||
.then(({data}) => {
|
||||
this.allTags = data.allTags
|
||||
this.noteTags = data.noteTagIds
|
||||
|
||||
//Stick used tags at top.
|
||||
if(this.noteTags.length > 0){
|
||||
|
||||
let frontTags = []
|
||||
|
||||
for (var i = this.allTags.length - 1; i >= 0; i--) {
|
||||
this.noteTags.forEach(noteTag => {
|
||||
if(this.allTags[i]['id'] == noteTag['tagId']){
|
||||
frontTags.push(this.allTags[i])
|
||||
this.allTags.splice(i,1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.allTags.unshift(...frontTags)
|
||||
}
|
||||
})
|
||||
},
|
||||
isTagOnNote(id){
|
||||
for (let i = 0; i < this.noteTags.length; i++) {
|
||||
const current = this.noteTags[i]
|
||||
if(current && current['tagId'] == id){
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
initSquire(){
|
||||
|
||||
//Set up squire and load note text
|
||||
@ -458,8 +542,8 @@
|
||||
if(!this.$store.getters.getIsUserOnMobile){
|
||||
this.editor.focus()
|
||||
this.editor.moveCursorToEnd()
|
||||
this.caretShow = true
|
||||
this.moveArtificialCaret()
|
||||
|
||||
this.fetchNoteTags() //Don't load tags on mobile
|
||||
}
|
||||
|
||||
|
||||
@ -474,9 +558,6 @@
|
||||
this.lastRange = e.range
|
||||
this.startOffset = parseInt(e.range.startOffset)
|
||||
return
|
||||
|
||||
// const rect = e.range.getBoundingClientRect()
|
||||
// this.moveArtificialCaret(rect)
|
||||
})
|
||||
|
||||
//Change button states on editor when element is active
|
||||
@ -532,14 +613,7 @@
|
||||
})
|
||||
|
||||
this.editor.addEventListener('keydown', event => {
|
||||
setTimeout(() => {
|
||||
if(event.keyCode == 32){
|
||||
this.caretLeft += 3
|
||||
}
|
||||
if(event.keyCode == 8){
|
||||
// this.caretLeft -= 3
|
||||
}
|
||||
}, 10)
|
||||
|
||||
})
|
||||
|
||||
//Bind event handlers
|
||||
@ -550,29 +624,10 @@
|
||||
|
||||
//Show and hide additional toolbars
|
||||
this.editor.addEventListener('focus', e => {
|
||||
// this.caretShow = true
|
||||
})
|
||||
this.editor.addEventListener('blur', e => {
|
||||
// this.caretShow = false
|
||||
})
|
||||
},
|
||||
moveArtificialCaret(rect = null){
|
||||
|
||||
//Lets not use the artificial caret for now
|
||||
return
|
||||
|
||||
//If rect isn't present, grab by selection
|
||||
if(!rect || rect.left == 0){ //Left should always be greater than 0, because of a margin
|
||||
rect = this.editor.getCursorPosition()
|
||||
//Another way to get range
|
||||
// window.getSelection().getRangeAt(0)
|
||||
}
|
||||
|
||||
const textArea = document.getElementById('text-box-container').getBoundingClientRect()
|
||||
|
||||
this.caretLeft = (rect.left - textArea.left - 1)
|
||||
this.caretTop = (rect.top - textArea.top - 1 )
|
||||
},
|
||||
openEditAttachment(){
|
||||
|
||||
this.$router.push('/attachments/note/'+this.currentNoteId)
|
||||
@ -762,7 +817,7 @@
|
||||
// clearTimeout(this.editDebounce)
|
||||
|
||||
if(this.statusText == 'saving'){
|
||||
return reject(false)
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
//Don't save note if its hash doesn't change
|
||||
@ -995,26 +1050,60 @@
|
||||
margin: 45px 0 45px 0;
|
||||
position: relative;
|
||||
}
|
||||
.artificial-caret {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 21px;
|
||||
width: 1.5px;
|
||||
margin: 0 0;
|
||||
/*opacity: 0.3;*/
|
||||
background-color: rgb(33, 186, 69);
|
||||
animation: blinker 1.1s ease-in-out infinite;
|
||||
}
|
||||
/* .artificial-caret:after {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
margin: 0 0 0 -1px;
|
||||
display: block;
|
||||
background-color: rgb(33, 186, 69);
|
||||
|
||||
}*/
|
||||
.note-mini-tag-area {
|
||||
position: fixed;
|
||||
width: 120px;
|
||||
left: calc(15% - 125px);
|
||||
top: 46px;
|
||||
bottom: 0;
|
||||
height: calc(100vh - 55px);
|
||||
z-index: 1000;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
.note-mini-tag-area {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: inherit inherit;
|
||||
}
|
||||
.subtle-tag {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 1px 1px 1px 5px;
|
||||
margin: 0 0 0;
|
||||
border: 1px solid transparent;
|
||||
border-right: none;
|
||||
border-radius: 3px;
|
||||
background-color: transparent;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color ease 0.3s, background ease 0.3s;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
text-transform:capitalize;
|
||||
}
|
||||
.note-mini-tag-area:hover .subtle-tag {
|
||||
opacity: 1;
|
||||
}
|
||||
.note-mini-tag-area:hover .active-mini-tag {
|
||||
background-color: var(--main-accent);
|
||||
color: white;
|
||||
}
|
||||
.note-mini-tag-area:hover .subtle-tag:not(.active-mini-tag) {
|
||||
border-right: none;
|
||||
color: var(--text_color);
|
||||
background-color: var(--body_bg_color);
|
||||
opacity: 1;
|
||||
}
|
||||
.active-mini-tag {
|
||||
opacity: 0.7;
|
||||
background-color: var(--small_element_bg_color);
|
||||
color: var(--text_color)
|
||||
}
|
||||
|
||||
|
||||
@keyframes blinker {
|
||||
50% {
|
||||
|
@ -32,7 +32,7 @@
|
||||
class="big-text"><p>{{ note.title }}</p></span>
|
||||
|
||||
<!-- Sub text display -->
|
||||
<span v-if="note.subtext.length > 0 && !isShowingSearchResults()"
|
||||
<span v-if="note.subtext.length > 0"
|
||||
class="small-text"
|
||||
v-html="note.subtext"></span>
|
||||
|
||||
@ -49,15 +49,6 @@
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- Display highlights from solr results -->
|
||||
<span v-if="note.note_highlights.length > 0" class="term-usage">
|
||||
<span
|
||||
class="usage-row"
|
||||
v-for="highlight in note.note_highlights"
|
||||
:class="{ 'big-text':(highlight <= 100), 'small-text-title':(highlight >= 100) }"
|
||||
v-html="cleanHighlight(highlight)"></span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="titleView" class="single-line-text" @click="cardClicked">
|
||||
@ -179,12 +170,6 @@
|
||||
|
||||
return updated
|
||||
},
|
||||
isShowingSearchResults(){
|
||||
if(this.note.note_highlights.length > 0 || this.note.attachment_highlights.length > 0 || this.note.tag_highlights.length > 0){
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
splitTags(text){
|
||||
return text.split(',')
|
||||
},
|
||||
|
@ -8,8 +8,12 @@
|
||||
<div class="ui grid" v-if="shareUsername == null">
|
||||
|
||||
<div v-if="!isNoteShared" class="sixteen wide column">
|
||||
<div class="ui button" v-on:click="makeShared()">Enable Shared</div>
|
||||
<p>Shared notes are different and junk.</p>
|
||||
<div class="ui button" v-on:click="makeShared()">Enable Sharing</div>
|
||||
<ul>
|
||||
<li>Shared notes can be read and edited by you and all shared users.</li>
|
||||
<li>Shared notes can only be shared by the creator of the note.</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="isNoteShared" class="sixteen wide column">
|
||||
@ -17,14 +21,10 @@
|
||||
<div class="ui button" v-on:click="removeShared()">Remove Shared</div>
|
||||
|
||||
<div class="ui button" v-on:click="getSharedUrl()">Get Shareable URL</div>
|
||||
|
||||
<div v-if="sharedUrl.length > 0">
|
||||
<a target="_blank" :href="sharedUrl">{{ sharedUrl }}</a>
|
||||
<div class="ui input">
|
||||
<input type="text" v-model="sharedUrl">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="sixteen wide column" v-if="isNoteShared && sharedUrl.length > 0">
|
||||
<p>Public Link - this link can be disabled by turning off sharing</p>
|
||||
<a target="_blank" :href="sharedUrl">{{ sharedUrl }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -3,19 +3,27 @@
|
||||
<div class="ui grid">
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui text container">
|
||||
<div class="ui raised segment">
|
||||
|
||||
|
||||
<h2 class="ui dividing header">
|
||||
<i class="green question circle outline icon"></i>
|
||||
Help
|
||||
</h2>
|
||||
|
||||
<p v-if="$store.getters.totals && $store.getters.totals['activeSessions'] > 0">
|
||||
Logged in user has {{ $store.getters.totals['activeSessions'] }} active sessions.
|
||||
</p>
|
||||
|
||||
<!-- Content copied from note -->
|
||||
<!-- https://www.solidscribe.com/#/notes/open/552 -->
|
||||
|
||||
<p><b>Only Note Text is Encrypted</b><br></p><p>Only you can read your notes. Encryption is the transformation of data into a form unreadable by anyone without the password. Its purpose is to ensure privacy by keeping the information hidden from anyone for whom it is not intended, even those who can see the encrypted data. 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>The Scrat</b><b></b><b>ch Pad</b><b></b><br></p><p>The Scratch Pad 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><ul><li>All data pushed to the quick note can still be edited like a normal note.<br></li><li>Creating a new quick note will not erase your old <br></li></ul><p><br></p><p><b>Flux Theme</b><br></p><p>Flux theme limits the amount of blue emitted by your screen. Most things turn sepia and a filter is applied to images to make them more sepia. Less blue light at night is supposed to be helpful for falling asleep.<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>Keyboard Shor</b><b></b><b>tcuts</b><br></p><p>Number List - CTRL + SHIFT + 9<br></p><p>Todo List - CTRL + SHIFT + 8<br></p><p>Underline CTRL + u<br></p><p>Bold - CTRL + b<br></p><p>Quote - CRTL + i<br></p><p>Indent - CTL + ]<br></p><p>Outdent - CRTL + [<br></p><p>Undo - CTRL + z<br></p><p>Redo - CTRL + y<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. You can edit the text of scarped links and any time and search for it later. <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 text scanned, 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>Only Note Text is Encrypted</b><br></p><blockquote><p>Only you can read your notes. Encryption is the transformation of data into a form unreadable by anyone without the password. Its purpose is to ensure privacy by keeping the information hidden from anyone for whom it is not intended, even those who can see the encrypted data. 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></blockquote><p><br></p><p><b>Some Data is not encrypted</b><br></p><blockquote><p>Everything isn't encrypted, to keep up ease of use. Files, Tags and Attachments are not encrypted.<br></p></blockquote><p><br></p><p><b>Searching is somewhat limited</b><br></p><blockquote><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></blockquote><p><br></p><p><b>The Scrat</b><b></b><b>ch Pad</b><b></b><br></p><blockquote><p>The Scratch Pad 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><ul><li>All data pushed to the quick note can still be edited like a normal note.<br></li><li>Creating a new quick note will not erase your old <br></li></ul></blockquote><p><br></p><p><b>Flux Theme</b><br></p><blockquote><p>Flux theme limits the amount of blue emitted by your screen. Most things turn sepia and a filter is applied to images to make them more sepia. Less blue light at night is supposed to be helpful for falling asleep.<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></blockquote><p><br></p><p><b>Text Editor Keyboard Shor</b><b>tcuts</b><br></p><blockquote><p>Number List - CTRL + SHIFT + 9<br></p><p>Todo List - CTRL + SHIFT + 8<br></p><p>Underline CTRL + u<br></p><p>Bold - CTRL + b<br></p><p>Quote - CRTL + i<br></p><p>Indent - CTL + ]<br></p><p>Outdent - CRTL + [<br></p><p>Undo - CTRL + z<br></p><p>Redo - CTRL + y<br></p></blockquote><p><br></p><p><b>Shared Notes and Security</b><br></p><blockquote><p>Shared notes still respect privacy but use a different security scheme. Instead of encrypting the note with your password, a shared password is created. This note then uses <a href="https://en.wikipedia.org/wiki/Public-key_cryptography">public-key cryptography</a> to share the note with other users, while still making it unreadable to anyone else. <br></p><ul><li>You can revoke any users access to a shared note at any time.<br></li><li>Notes you share can not be shared by other people.<br></li><li>If you turn off sharing, all users lose access and the note is reverted to the default security scheme.<br></li></ul></blockquote><p><b>Public Links for shared notes</b><br></p><blockquote><p>It is possible to generate a public URL for shared notes. This is a huge security risk and exposes the Encryption Key for the note. The encryption key to a shared note can easily be changed by turning off sharing, then turning it back on. <br></p><p><br></p></blockquote><p><b>Links in notes</b><br></p><blockquote><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. You can edit the text of scarped links and any time and search for it later. <br></p></blockquote><p><br></p><p><b>Files in notes</b><br></p><blockquote><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 text scanned, 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></blockquote><p><br></p><p><b>Deleting notes</b><br></p><blockquote><p>When<b> </b>notes are deleted, none of the files related to the note are deleted. <br></p></blockquote><p><br></p><p><b>Daily Backups</b><br></p><blockquote><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></blockquote><p><br></p>
|
||||
|
||||
<!-- content copied from note -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -10,10 +10,11 @@
|
||||
-webkit-animation: fadeorama 16s ease infinite;
|
||||
-moz-animation: fadeorama 16s ease infinite;
|
||||
animation: fadeorama 16s ease infinite;
|
||||
height: 350px;
|
||||
}
|
||||
.logo-display {
|
||||
width: 50%;
|
||||
max-width: 450px;
|
||||
width: 140px;
|
||||
height: auto;
|
||||
}
|
||||
.lightly-padded {
|
||||
margin-top: 10px;
|
||||
@ -112,10 +113,10 @@
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg" alt="">
|
||||
</div>
|
||||
|
||||
<div class="one wide large screen only column"></div>
|
||||
<!-- <div class="one wide large screen only column"></div> -->
|
||||
|
||||
<!-- desktop column - large screen only -->
|
||||
<div class="seven wide middle aligned left aligned column">
|
||||
<div class="sixteen wide middle aligned center aligned column">
|
||||
|
||||
<h2 class="massive-text">
|
||||
<img class="logo-display" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
|
||||
@ -129,9 +130,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="eight wide middle aligned left aligned column">
|
||||
<!-- <div class="eight wide middle aligned left aligned column">
|
||||
<img loading="lazy" width="90%" src="/api/static/assets/marketing/notebook.svg" alt="The Venus fly laptop about to capture another victim">
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
@ -341,12 +342,6 @@ export default {
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
//Don't change hero banner on mobile
|
||||
if(!this.$store.getters.getIsUserOnMobile){
|
||||
let windowHeight = window.innerHeight
|
||||
this.height = windowHeight - (windowHeight * 0.18)
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
showRealInformation(){
|
||||
|
@ -117,7 +117,7 @@
|
||||
:data="note"
|
||||
:title-view="titleView"
|
||||
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
|
||||
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
|
||||
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -509,7 +509,7 @@
|
||||
|
||||
//Don't move notes that were not changed
|
||||
if(note.updated == newNote.updated){
|
||||
return
|
||||
// return
|
||||
}
|
||||
|
||||
//go through each prop and update it with new values
|
||||
|
@ -6,30 +6,16 @@ Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
token: null,
|
||||
username: null,
|
||||
nightMode: false,
|
||||
isUserOnMobile: false,
|
||||
isNoteSettingsOpen: false, //Little note settings pane
|
||||
socket: null,
|
||||
userTotals: null,
|
||||
},
|
||||
mutations: {
|
||||
setLoginToken(state, userData){
|
||||
|
||||
const username = userData.username
|
||||
const token = userData.token
|
||||
|
||||
localStorage.removeItem('loginToken') //We only want one login token per computer
|
||||
localStorage.setItem('loginToken', token)
|
||||
setUsername(state, username){
|
||||
|
||||
localStorage.removeItem('username') //We only want one login token per computer
|
||||
localStorage.setItem('username', username)
|
||||
|
||||
//Set default token to axios, every request will have header
|
||||
axios.defaults.headers.common['authorizationtoken'] = token
|
||||
|
||||
state.token = token
|
||||
state.username = username
|
||||
},
|
||||
destroyLoginToken(state){
|
||||
@ -37,8 +23,8 @@ export default new Vuex.Store({
|
||||
//Remove login token from local storage and from headers
|
||||
localStorage.removeItem('loginToken')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('currentVersion')
|
||||
delete axios.defaults.headers.common['authorizationtoken']
|
||||
state.token = null
|
||||
state.username = null
|
||||
},
|
||||
toggleNightMode(state, pastTheme){
|
||||
@ -125,6 +111,20 @@ export default new Vuex.Store({
|
||||
//Save all the totals for the user
|
||||
state.userTotals = totalsObject
|
||||
|
||||
//Set computer version from server
|
||||
const currentVersion = localStorage.getItem('currentVersion')
|
||||
if(currentVersion == null){
|
||||
localStorage.setItem('currentVersion', totalsObject.currentVersion)
|
||||
return
|
||||
}
|
||||
|
||||
//If version is already set and it doesn't match the server, reload app
|
||||
if(currentVersion != totalsObject.currentVersion){
|
||||
localStorage.setItem('currentVersion', totalsObject.currentVersion)
|
||||
location.reload(true)
|
||||
}
|
||||
|
||||
|
||||
// console.log('-------------')
|
||||
// Object.keys(totalsObject).forEach( key => {
|
||||
// console.log(key + ' -- ' + totalsObject[key])
|
||||
@ -135,11 +135,8 @@ export default new Vuex.Store({
|
||||
getUsername: state => {
|
||||
return state.username
|
||||
},
|
||||
getLoginToken: state => {
|
||||
return state.token
|
||||
},
|
||||
getLoggedIn: state => {
|
||||
let weIn = (state.token !== null && state.token != undefined && state.token.length > 0)
|
||||
let weIn = (state.username && state.username.length > 0)
|
||||
return weIn
|
||||
},
|
||||
getIsNightMode: state => {
|
||||
|
@ -1,38 +0,0 @@
|
||||
##
|
||||
#
|
||||
# This is just a mock config file, describing what is needed to run the app
|
||||
# The app currently only needs two paths / and /api
|
||||
#
|
||||
##
|
||||
|
||||
#
|
||||
# This is needed to define any ports the app may use from node
|
||||
#
|
||||
upstream expressapp {
|
||||
server 127.0.0.1:3000;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
#
|
||||
# Needed to server up static, compiled JS files and index.html
|
||||
#
|
||||
location / {
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
#
|
||||
# define the api route to connect to the backend and serve up static files
|
||||
#
|
||||
location /api {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
|
||||
proxy_pass http://expressapp;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
}
|
25
package-lock.json
generated
25
package-lock.json
generated
@ -4,11 +4,6 @@
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@log4js-node/log4js-api": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@log4js-node/log4js-api/-/log4js-api-1.0.2.tgz",
|
||||
"integrity": "sha1-eoFD+zPwd98+V53Kfxj+p0oC7Is="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz",
|
||||
@ -1209,11 +1204,6 @@
|
||||
"resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz",
|
||||
"integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha1-5jNFY4bUqlWGP2dqerDaqP3ssP0="
|
||||
},
|
||||
"node-tesseract-ocr": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-tesseract-ocr/-/node-tesseract-ocr-1.0.0.tgz",
|
||||
@ -1592,16 +1582,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"solr-node": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/solr-node/-/solr-node-1.2.1.tgz",
|
||||
"integrity": "sha1-vZkXswJRp+N+fg2+nBtZnzl4zwY=",
|
||||
"requires": {
|
||||
"@log4js-node/log4js-api": "^1.0.2",
|
||||
"node-fetch": "^2.3.0",
|
||||
"underscore": "^1.8.3"
|
||||
}
|
||||
},
|
||||
"sqlstring": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
|
||||
@ -1714,11 +1694,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz",
|
||||
"integrity": "sha1-BtzjSg5op7q8KbNluOdLiSUgOWE="
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
@ -23,8 +23,7 @@
|
||||
"node-tesseract-ocr": "^1.0.0",
|
||||
"request": "^2.88.0",
|
||||
"request-promise": "^4.2.4",
|
||||
"socket.io": "^2.3.0",
|
||||
"solr-node": "^1.2.1"
|
||||
"socket.io": "^2.3.0"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"@root": ".",
|
||||
|
@ -1,25 +1,151 @@
|
||||
var jwt = require('jsonwebtoken');
|
||||
const db = require('@config/database')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const cs = require('@helpers/CryptoString')
|
||||
|
||||
let Auth = {}
|
||||
|
||||
const tokenSecretKey = process.env.JSON_KEY
|
||||
|
||||
Auth.createToken = (userId, masterKey) => {
|
||||
const signedData = {'id':userId, 'date':Date.now(), 'masterKey':masterKey}
|
||||
const token = jwt.sign(signedData, tokenSecretKey)
|
||||
return token
|
||||
}
|
||||
Auth.decodeToken = (token) => {
|
||||
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, tokenSecretKey, function(err, decoded){
|
||||
if(err || decoded.id == undefined){
|
||||
reject('Bad Token')
|
||||
return
|
||||
}
|
||||
//Pass back decoded token
|
||||
resolve(decoded)
|
||||
return
|
||||
});
|
||||
|
||||
const created = pastCreatedDate ? pastCreatedDate : Math.floor(+new Date/1000)
|
||||
const userHash = cs.hash(String(userId)).toString('base64')
|
||||
|
||||
//Encrypt Master Password and save it to the server
|
||||
const sessionId = pastId ? pastId : cs.createSmallSalt().slice(0,9) //Use existing session id
|
||||
const salt = cs.createSmallSalt()
|
||||
const tempPass = cs.createSmallSalt()
|
||||
const encryptedMasterPass = cs.encrypt(tempPass, salt, masterKey)
|
||||
|
||||
//Deactivate all other session keys, they delete after 30 seconds
|
||||
db.promise().query('UPDATE user_active_session SET active = 0 WHERE session_id = ?', [sessionId])
|
||||
.then((r,f) => {
|
||||
|
||||
return db.promise().query(
|
||||
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)',
|
||||
[salt, encryptedMasterPass, created, 40, userHash, sessionId])
|
||||
|
||||
})
|
||||
.then((r,f) => {
|
||||
|
||||
const sessionNum = r[0].insertId
|
||||
|
||||
//Required Data for JWT payload
|
||||
const tokenPayload = {userId, tempPass, sessionNum}
|
||||
|
||||
//Return token
|
||||
const token = jwt.sign(tokenPayload, tokenSecretKey)
|
||||
return resolve(token)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Auth.decodeToken = (token, request = null) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let decodedToken = null
|
||||
|
||||
//Delete all tokens older than 20 days before continuing or inacive and older than 1 minute
|
||||
const now = (Math.floor((+new Date)/1000))
|
||||
const twentyDays = (Math.floor((+new Date)/1000)) - (86400 * 20)
|
||||
const thirtySeconds = (Math.floor((+new Date)/1000)) - (30)
|
||||
|
||||
//Decode Json web token
|
||||
jwt.verify(token, tokenSecretKey, function(err, decoded){
|
||||
if(err || decoded.tempPass == undefined || decoded.tempPass.length < 5){
|
||||
throw new Error('Bad Token')
|
||||
}
|
||||
|
||||
decodedToken = decoded
|
||||
|
||||
db.promise().query('DELETE from user_active_session WHERE (created < ?) OR (active = false AND last_used < ?)', [twentyDays, thirtySeconds])
|
||||
.then((r,f) => {
|
||||
|
||||
//Lookup session data in database
|
||||
db.promise().query('SELECT * FROM user_active_session WHERE id = ? LIMIT 1', [decodedToken.sessionNum])
|
||||
.then((r,f) => {
|
||||
|
||||
if(r == undefined || r[0].length == 0){
|
||||
throw new Error('Active Session not found for token')
|
||||
}
|
||||
|
||||
const row = r[0][0]
|
||||
|
||||
// console.log(decodedToken.sessionNum + ' uses -> ' + row.uses)
|
||||
|
||||
if(row.uses <= 0){
|
||||
throw new Error('Token is used up')
|
||||
}
|
||||
|
||||
//Decrypt master key from lookup
|
||||
const masterKey = cs.decrypt(decodedToken.tempPass, row.salt, row.encrypted_master_password)
|
||||
if(masterKey == null){
|
||||
// console.log('Deleting invalid session')
|
||||
Auth.terminateSession(row.session_id)
|
||||
throw new Error ('Unable to decrypt password for session')
|
||||
}
|
||||
|
||||
//Async update DB counts and disable session if needed
|
||||
db.promise().query('UPDATE user_active_session SET uses = uses -1, last_used = ? WHERE id = ? LIMIT 1', [now, decodedToken.sessionNum])
|
||||
.then((r,f) => {
|
||||
|
||||
let userData = {
|
||||
'userId': decodedToken.userId,
|
||||
'masterKey': masterKey,
|
||||
'sessionId': row.session_id,
|
||||
'created': row.created,
|
||||
'remainingUses':(row.uses--),
|
||||
'active': row.active
|
||||
}
|
||||
|
||||
//Return token Data
|
||||
return resolve(userData)
|
||||
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
//Token errors result in having sessions deleted
|
||||
// console.log('-- Auth Token Error --')
|
||||
// console.log(error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Auth.terminateSession = (sessionId) => {
|
||||
return db.promise().query('DELETE from user_active_session WHERE session_id = ?', [sessionId])
|
||||
}
|
||||
|
||||
Auth.deletAllLoginKeys = (userId) => {
|
||||
|
||||
const userHash = cs.hash(String(userId)).toString('base64')
|
||||
|
||||
return db.promise().query('DELETE FROM user_active_session WHERE user_hash = ?', [userHash])
|
||||
}
|
||||
|
||||
Auth.test = () => {
|
||||
|
||||
const testUserId = 22
|
||||
const testPass = cs.createSmallSalt()
|
||||
Auth.createToken(testUserId, testPass)
|
||||
.then(token => {
|
||||
|
||||
console.log('Test: Create JWT -> Pass')
|
||||
|
||||
return Auth.decodeToken(token)
|
||||
})
|
||||
.then(userData => {
|
||||
|
||||
console.log('Test: Decrypted key Match -> ' + (testPass == userData.masterKey))
|
||||
return Auth.deletAllLoginKeys(testUserId)
|
||||
})
|
||||
.then(results => {
|
||||
|
||||
console.log('Test: Remove user Json Web Tokens - Pass')
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,9 @@ CryptoString.encrypt = (password, salt64, rawText) => {
|
||||
//Decrypt base64 string cipher text,
|
||||
CryptoString.decrypt = (password, salt64, cipherTextString) => {
|
||||
|
||||
if(!password || !salt64 || !cipherTextString){ return '' }
|
||||
if(password.length == 0 || salt64.length == 0 || cipherTextString == 0){ return '' }
|
||||
|
||||
let cipherText = Buffer.from(cipherTextString, 'base64')
|
||||
const salt = Buffer.from(salt64, 'base64')
|
||||
|
||||
|
@ -69,7 +69,7 @@ ProcessText.deduceNoteTitle = (inTitle, inString) => {
|
||||
//Remove inline styles that may be added by editor
|
||||
// inString = inString.replace(/style=".*?"/g,'')
|
||||
|
||||
const tagFreeLength = ProcessText.removeHtml(inString).length
|
||||
// const tagFreeLength = ProcessText.removeHtml(inString).length
|
||||
|
||||
//
|
||||
// Simplified attempt!
|
||||
@ -80,7 +80,7 @@ ProcessText.deduceNoteTitle = (inTitle, inString) => {
|
||||
// if(tagFreeLength > 200){
|
||||
// sub += '... <i class="green caret down icon"></i>'
|
||||
// }
|
||||
inString += '</end>'
|
||||
// inString += '</end>'
|
||||
|
||||
return {title, sub}
|
||||
|
||||
|
@ -50,13 +50,37 @@ io.on('connection', function(socket){
|
||||
socket.on('user_connect', token => {
|
||||
Auth.decodeToken(token)
|
||||
.then(userData => {
|
||||
socket.join(userData.id)
|
||||
socket.join(userData.userId)
|
||||
}).catch(error => {
|
||||
//Don't add user to room if they are not logged in
|
||||
// console.log(error)
|
||||
})
|
||||
})
|
||||
|
||||
//Renew Session tokens when users request a new one
|
||||
socket.on('renew_session_token', token => {
|
||||
|
||||
//Decode the token they currently have
|
||||
Auth.decodeToken(token)
|
||||
.then(userData => {
|
||||
|
||||
console.log('Is active -> ', userData.active)
|
||||
|
||||
if(userData.active == 1){
|
||||
//Create a new one using credentials and session keys from current
|
||||
Auth.createToken(userData.userId, userData.masterKey, userData.sessionId, userData.created)
|
||||
.then(newToken => {
|
||||
|
||||
//Emit new token only to user on socket
|
||||
socket.emit('recievend_new_token', newToken)
|
||||
})
|
||||
} else {
|
||||
//Attempting to reactivate disabled session, kills it all
|
||||
Auth.terminateSession(userData.sessionId)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('join_room', rawTextId => {
|
||||
// Join user to rawtextid room when they enter
|
||||
socket.join(rawTextId)
|
||||
@ -78,11 +102,7 @@ io.on('connection', function(socket){
|
||||
//Update users in room count
|
||||
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
|
||||
|
||||
//Debugging text
|
||||
console.log('Note diff object')
|
||||
console.log(noteDiffs)
|
||||
|
||||
|
||||
//Debugging text - prints out notes in limbo
|
||||
let noteDiffKeys = Object.keys(noteDiffs)
|
||||
let totalDiffs = 0
|
||||
noteDiffKeys.forEach(diffSetKey => {
|
||||
@ -90,9 +110,11 @@ io.on('connection', function(socket){
|
||||
totalDiffs += noteDiffs[diffSetKey].length
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Total notes in limbo -> ', noteDiffKeys.length)
|
||||
console.log('Total Diffs for all notes -> ', totalDiffs)
|
||||
//Debugging Text
|
||||
if(noteDiffKeys.length > 0){
|
||||
console.log('Total notes in limbo -> ', noteDiffKeys.length)
|
||||
console.log('Total Diffs for all notes -> ', totalDiffs)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
@ -198,21 +220,32 @@ http.listen(3001, function(){
|
||||
//Enable json body parsing in requests. Allows me to post data in ajax calls
|
||||
app.use(express.json({limit: '5mb'}))
|
||||
|
||||
|
||||
//Prefix defied by route in nginx config
|
||||
const prefix = '/api'
|
||||
|
||||
//App Auth, all requests will come in with a token, decode the token and set global var
|
||||
app.use(function(req, res, next){
|
||||
|
||||
//Always null out master key, never allow it set from outside
|
||||
req.headers.masterKey = null
|
||||
req.headers.sessionId = null
|
||||
|
||||
//auth token set by axios in headers
|
||||
let token = req.headers.authorizationtoken
|
||||
if(token && token != null && typeof token === 'string'){
|
||||
Auth.decodeToken(token)
|
||||
if(token !== undefined && token.length > 0){
|
||||
Auth.decodeToken(token, req)
|
||||
.then(userData => {
|
||||
req.headers.userId = userData.id //Update headers for the rest of the application
|
||||
|
||||
//Update headers for the rest of the application
|
||||
req.headers.userId = userData.userId
|
||||
req.headers.masterKey = userData.masterKey
|
||||
req.headers.sessionId = userData.sessionId
|
||||
|
||||
//Tell front end remaining uses on current token
|
||||
res.set('remainingUses', userData.remainingUses)
|
||||
|
||||
next()
|
||||
}).catch(error => {
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
console.log(error)
|
||||
|
||||
res.statusMessage = error //Throw 400 error if token is bad
|
||||
res.status(400).end()
|
||||
@ -224,10 +257,12 @@ app.use(function(req, res, next){
|
||||
|
||||
|
||||
// Test Area
|
||||
const printResults = false
|
||||
const printResults = true
|
||||
let UserTest = require('@models/User')
|
||||
let NoteTest = require('@models/Note')
|
||||
UserTest.keyPairTest('genMan12', '1', printResults)
|
||||
let AuthTest = require('@helpers/Auth')
|
||||
Auth.test()
|
||||
UserTest.keyPairTest('genMan15', '1', printResults)
|
||||
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
|
||||
.then( message => {
|
||||
if(printResults) console.log(message)
|
||||
@ -236,34 +271,34 @@ UserTest.keyPairTest('genMan12', '1', printResults)
|
||||
|
||||
|
||||
//Test
|
||||
app.get(prefix, (req, res) => res.send('The api is running'))
|
||||
app.get('/api', (req, res) => res.send('Solidscribe API is up and running'))
|
||||
|
||||
//Serve up uploaded files
|
||||
app.use(prefix+'/static', express.static( __dirname+'/../staticFiles' ))
|
||||
app.use('/api/static', express.static( __dirname+'/../staticFiles' ))
|
||||
|
||||
//Public routes
|
||||
var public = require('@routes/publicController')
|
||||
app.use(prefix+'/public', public)
|
||||
app.use('/api/public', public)
|
||||
|
||||
//user endpoint
|
||||
var user = require('@routes/userController')
|
||||
app.use(prefix+'/user', user)
|
||||
app.use('/api/user', user)
|
||||
|
||||
//notes endpoint
|
||||
var notes = require('@routes/noteController')
|
||||
app.use(prefix+'/note', notes)
|
||||
app.use('/api/note', notes)
|
||||
|
||||
//tags endpoint
|
||||
var tags = require('@routes/tagController')
|
||||
app.use(prefix+'/tag', tags)
|
||||
app.use('/api/tag', tags)
|
||||
|
||||
//notes endpoint
|
||||
var attachment = require('@routes/attachmentController')
|
||||
app.use(prefix+'/attachment', attachment)
|
||||
app.use('/api/attachment', attachment)
|
||||
|
||||
//quick notes endpoint
|
||||
var quickNote = require('@routes/quicknoteController')
|
||||
app.use(prefix+'/quick-note', quickNote)
|
||||
app.use('/api/quick-note', quickNote)
|
||||
|
||||
//Output running status
|
||||
app.listen(port, () => {
|
||||
|
@ -454,9 +454,12 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
|
||||
}
|
||||
|
||||
let encryptedNoteText = ''
|
||||
//Create encrypted snippet
|
||||
const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
|
||||
noteSnippet = cs.encrypt(masterKey, snippetSalt, snippet)
|
||||
//Create encrypted snippet if its a long note
|
||||
let snippet = ''
|
||||
if(noteText.length > 500){
|
||||
snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
|
||||
noteSnippet = cs.encrypt(masterKey, snippetSalt, snippet)
|
||||
}
|
||||
|
||||
//Encrypt note text
|
||||
const textObject = JSON.stringify([noteTitle, noteText])
|
||||
@ -946,9 +949,11 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
||||
let searchParams = [userId]
|
||||
let noteSearchQuery = `
|
||||
SELECT note.id,
|
||||
note.snippet as snippet,
|
||||
note.snippet_salt as salt,
|
||||
note_raw_text.updated as updated,
|
||||
note.snippet as snippetText,
|
||||
note.snippet_salt as snippetSalt,
|
||||
note_raw_text.text as noteText,
|
||||
note_raw_text.salt as noteSalt,
|
||||
note_raw_text.updated as updated,
|
||||
opened,
|
||||
color,
|
||||
count(distinct note_tag.id) as tag_count,
|
||||
@ -1092,26 +1097,39 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
||||
}
|
||||
|
||||
|
||||
//Decrypt note text
|
||||
if(note.snippet && note.salt){
|
||||
const decipheredText = cs.decrypt(currentNoteKey, note.salt, note.snippet)
|
||||
const textObject = JSON.parse(decipheredText)
|
||||
if(textObject != null && textObject.length == 2){
|
||||
note.title = textObject[0]
|
||||
note.text = textObject[1]
|
||||
}
|
||||
//Only long notes have snippets, decipher it if present
|
||||
let displayTitle = ''
|
||||
let displayText = ''
|
||||
|
||||
let encryptedText = note.noteText
|
||||
let relatedSalt = note.noteSalt
|
||||
|
||||
//Default to note text, use snippet if set
|
||||
if(note.snippetSalt && note.snippetText && note.snippetSalt.length > 0 && note.snippetText.length > 0){
|
||||
encryptedText = note.snippetText
|
||||
relatedSalt = note.snippetSalt
|
||||
}
|
||||
|
||||
//Deduce note title
|
||||
const textData = ProcessText.deduceNoteTitle(note.title, note.text)
|
||||
|
||||
note.title = textData.title
|
||||
note.subtext = textData.sub
|
||||
try {
|
||||
const decipheredText = cs.decrypt(currentNoteKey, relatedSalt, encryptedText)
|
||||
const textObject = JSON.parse(decipheredText)
|
||||
if(textObject != null && textObject.length == 2){
|
||||
if(textObject[0] && textObject[0] != null && textObject[0].length > 0){
|
||||
displayTitle = textObject[0]
|
||||
}
|
||||
if(textObject[1] && textObject[1] != null && textObject[1].length > 0){
|
||||
displayText = textObject[1]
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.log('Error opening note id -> ', note.id)
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
//Remove these variables
|
||||
note.note_highlights = []
|
||||
note.attachment_highlights = []
|
||||
note.tag_highlights = []
|
||||
|
||||
|
||||
note.title = displayTitle
|
||||
note.subtext = ProcessText.stripDoubleBlankLines(displayText)
|
||||
|
||||
//Limit number of attachment thumbs to 4
|
||||
if(note.thumbs){
|
||||
@ -1123,9 +1141,12 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
||||
}
|
||||
|
||||
//Clear out note.text before sending it to front end, its being used in title and subtext
|
||||
delete note.snippet
|
||||
delete note.salt
|
||||
delete note.snippetText
|
||||
delete note.snippetSalt
|
||||
delete note.noteText
|
||||
delete note.noteSalt
|
||||
delete note.encrypted_share_password_key
|
||||
delete note.text //Passed back as title and subtext
|
||||
})
|
||||
|
||||
|
||||
|
@ -40,9 +40,10 @@ User.login = (username, password) => {
|
||||
.then(({publicKey, privateKey}) => {
|
||||
|
||||
//Passback a json web token
|
||||
const token = Auth.createToken(lookedUpUser.id, masterKey)
|
||||
resolve({ token: token, userId:lookedUpUser.id })
|
||||
|
||||
Auth.createToken(lookedUpUser.id, masterKey)
|
||||
.then(token => {
|
||||
return resolve({ token: token, userId:lookedUpUser.id })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -119,7 +120,10 @@ User.register = (username, password) => {
|
||||
})
|
||||
.then(({publicKey, privateKey}) => {
|
||||
|
||||
const token = Auth.createToken(userId, newMasterKey)
|
||||
return Auth.createToken(userId, newMasterKey)
|
||||
})
|
||||
.then(token => {
|
||||
|
||||
return resolve({token, userId})
|
||||
})
|
||||
.catch(console.log)
|
||||
@ -139,6 +143,7 @@ User.getCounts = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let countTotals = {}
|
||||
const userHash = cs.hash(String(userId)).toString('base64')
|
||||
|
||||
db.promise().query(
|
||||
`SELECT
|
||||
@ -166,6 +171,14 @@ User.getCounts = (userId) => {
|
||||
|
||||
Object.assign(countTotals, rows[0][0]) //combine results
|
||||
|
||||
return db.promise().query(
|
||||
`SELECT count(id) as activeSessions FROM user_active_session WHERE user_hash = ?`, [userHash]
|
||||
)
|
||||
})
|
||||
.then( (rows, fields) => {
|
||||
|
||||
Object.assign(countTotals, rows[0][0]) //combine results
|
||||
|
||||
return db.promise().query(
|
||||
`SELECT
|
||||
SUM(attachment_type = 1) as linkFiles,
|
||||
@ -185,12 +198,20 @@ User.getCounts = (userId) => {
|
||||
countTotals[key] = count ? count : 0
|
||||
})
|
||||
|
||||
countTotals['currentVersion'] = '3.0.0'
|
||||
|
||||
resolve(countTotals)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//Log out user by deleting login token for that active session
|
||||
User.logout = (sessionId) => {
|
||||
console.log('Terminate Session -> ', sessionId)
|
||||
return db.promise().query('DELETE FROM user_active_session WHERE (session_id = ?)', [sessionId])
|
||||
}
|
||||
|
||||
User.generateMasterKey = (userId, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
|
@ -136,19 +136,4 @@ router.post('/disableshare', function (req, res) {
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Testing Action
|
||||
//
|
||||
//Reindex all Note. Not a very good function, not public
|
||||
router.get('/reindex5yu43prchuj903mrc', function (req, res) {
|
||||
|
||||
Note.migrateNoteTextToNewTable().then(status => {
|
||||
return res.send(status)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
module.exports = router
|
@ -30,6 +30,18 @@ router.post('/login', function (req, res) {
|
||||
res.send(false)
|
||||
})
|
||||
})
|
||||
// Logout User
|
||||
router.post('/logout', function (req, res) {
|
||||
|
||||
User.logout(req.headers.sessionId)
|
||||
.then( returnData => {
|
||||
res.send(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
// Login User
|
||||
router.post('/register', function (req, res) {
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user