Added privacy policy

Updated marketing
Added some keyboard shortcuts
Added settings page
Added accent theming
Added beta 2FA
This commit is contained in:
Max G committed 2020-07-07 04:04:55 +00:00
1 parent cca89a60d8
commit 47fff0e1ee
29 files changed
+1428 -362

No files matched your search

+6
View File
@@ -97,6 +97,12 @@ export default {
//Detect if user is on a mobile browser and set a flag in store
this.$store.commit('detectIsUserOnMobile')
//Set Main theme color
const accentColor = localStorage.getItem('main-accent')
if(accentColor){
document.documentElement.style.setProperty('--main-accent', accentColor)
}
//Set color theme based on local storage
const themeNumber = localStorage.getItem('nightMode')
if(themeNumber != null){
+4
View File
@@ -4,6 +4,10 @@ const helpers = {}
helpers.timeAgo = (time) => {
if(time == null){
time = Math.round(time/1000)
}
if(time.toString().length >= 13){
time = Math.round(time/1000)
}
+70 -5
View File
@@ -18,7 +18,8 @@
:root {
--main-accent: #16ab39;
/*main accent for all buttons, icons and logos*/
--main-accent: #21BA45;
/*theme colors */
--body_bg_color: #f5f6f7;
@@ -105,7 +106,7 @@ div.ui.basic.green.label {
background-color: var(--small_element_bg_color) !important;
}
.ui.basic.button, .ui.basic.buttons .button {
background-color: var(--small_element_bg_color) !important;
background-color: var(--small_element_bg_color);
color: var(--text_color) !important;
border: 1px solid;
border-color: var(--dark_border_color) !important;
@@ -125,6 +126,24 @@ div.ui.basic.green.label {
color: var(--text_color) !important;
border-color: var(--dark_border_color) !important;
}
/*Overwrites for modifiable theme color */
i.green.icon.icon.icon.icon {
color: var(--main-accent);
}
.ui.green.buttons, .ui.green.button, .ui.green.button:hover {
background-color: var(--main-accent);
}
.ui.basic.green.button, .ui.basic.green.buttons .button:hover, .ui.basic.green.button:hover, .ui.basic.green.button:focus {
box-shadow: var(--main-accent) 0px 0px 0px 1px inset;
}
.ui.green.labels .label, .ui.ui.ui.green.label {
background-color: var(--main-accent);
border-color: var(--main-accent);
}
.ui.grid > .green.row, .ui.grid > .green.column, .ui.grid > .row > .green.column {
background-color: var(--main-accent);
}
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
/*//
@@ -235,7 +254,7 @@ div.ui.basic.green.label {
/*border-bottom: 1px solid #ccc;*/
scrollbar-width: none;
scrollbar-color: transparent transparent;
caret-color: #21BA45;
caret-color: var(--main-accent);
}
.squire-box::selection,
.squire-box::-moz-selection {
@@ -253,7 +272,7 @@ div.ui.basic.green.label {
cursor: pointer;
}
.night-mode .note-card-text i:not(.icon),
.night-mode .squire-box i {
.night-mode .squire-box i:not(.icon) {
background-color: rgba(255, 255, 255, 0.2);
}
@@ -322,9 +341,55 @@ div.ui.basic.green.label {
font-family: 'Icons';
content: "\f058";
color: #21BA45;
color: var(--main-accent);
opacity: 1;
}
.note-title-display-card .divide,
.squire-box .divide {
width: 100%;
display: inline-block;
height: 2px;
background-color: var(--main-accent);
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
border-bottom: 1px solid #ddd;
font-weight: normal;
}
/* table:hover th, table:hover td {
border: 1px solid black;
}*/
th, td {
padding: 3px;
text-align: left;
}
.t-table {
width: 100%;
display: inline-block;
border: 1px solid black;
}
.t-table > span,
.t-table > div {
display: flex; /* aligns all child elements (flex items) in a row */
}
.t-table > span > span,
.t-table > div > div {
flex: 1; /* distributes space on the line equally among items */
border: 1px solid #DDD;
}
/* adjust checkboxes for mobile. Make them a little bigger, easier to click */
@media only screen and (max-width: 740px) {
+64 -16
View File
@@ -1096,6 +1096,9 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
}
while ( true ) {
if ( endContainer === endMax || endContainer === root ) {
break;
}
if ( maySkipBR &&
endContainer.nodeType !== TEXT_NODE &&
endContainer.childNodes[ endOffset ] &&
@@ -1103,9 +1106,7 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
endOffset += 1;
maySkipBR = false;
}
if ( endContainer === endMax ||
endContainer === root ||
endOffset !== getLength( endContainer ) ) {
if ( endOffset !== getLength( endContainer ) ) {
break;
}
parent = endContainer.parentNode;
@@ -1117,6 +1118,20 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
range.setEnd( endContainer, endOffset );
};
var moveRangeBoundaryOutOf = function ( range, nodeName, root ) {
var parent = getNearest( range.endContainer, root, 'A' );
if ( parent ) {
var clone = range.cloneRange();
parent = parent.parentNode;
moveRangeBoundariesUpTree( clone, parent, parent, root );
if ( clone.endContainer === parent ) {
range.setStart( clone.endContainer, clone.endOffset );
range.setEnd( clone.endContainer, clone.endOffset );
}
}
return range;
};
// Returns the first block at least partially contained by the range,
// or null if no block is contained by the range.
var getStartBlockOfRange = function ( range, root ) {
@@ -1285,10 +1300,13 @@ var onKey = function ( event ) {
if ( event.altKey ) { modifiers += 'alt-'; }
if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
if ( event.metaKey ) { modifiers += 'meta-'; }
if ( event.shiftKey ) { modifiers += 'shift-'; }
}
// However, on Windows, shift-delete is apparently "cut" (WTF right?), so
// we want to let the browser handle shift-delete.
if ( event.shiftKey ) { modifiers += 'shift-'; }
// we want to let the browser handle shift-delete in this situation.
if ( isWin && event.shiftKey && key === 'delete' ) {
modifiers += 'shift-';
}
key = modifiers + key;
@@ -1465,12 +1483,7 @@ var handleEnter = function ( self, shiftKey, range ) {
// just play it safe and insert a <br>.
if ( !block || shiftKey || /^T[HD]$/.test( block.nodeName ) ) {
// If inside an <a>, move focus out
parent = getNearest( range.endContainer, root, 'A' );
if ( parent ) {
parent = parent.parentNode;
moveRangeBoundariesUpTree( range, parent, parent, root );
range.collapse( false );
}
moveRangeBoundaryOutOf( range, 'A', root );
insertNodeInRange( range, self.createElement( 'BR' ) );
range.collapse( false );
self.setSelection( range );
@@ -1821,16 +1834,45 @@ if ( !isMac ) {
};
}
const changeIndentationLevel = function ( methodIfInQuote, methodIfInList ) {
return function ( self, event ) {
event.preventDefault();
var path = self.getPath();
if ( /(?:^|>)BLOCKQUOTE/.test( path ) ||
!/(?:^|>)[OU]L/.test( path ) ) {
self[ methodIfInQuote ]();
} else {
self[ methodIfInList ]();
}
};
};
const toggleList = function ( listRegex, methodIfNotInList ) {
return function ( self, event ) {
event.preventDefault();
var path = self.getPath();
if ( !listRegex.test( path ) ) {
self[ methodIfNotInList ]();
} else {
self.removeList();
}
};
};
keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' );
keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' );
keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' );
keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' );
keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } );
keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } );
keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' );
keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' );
keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
keyHandlers[ ctrlKey + 'shift-8' ] =
toggleList( /(?:^|>)UL/, 'makeUnorderedList' );
keyHandlers[ ctrlKey + 'shift-9' ] =
toggleList( /(?:^|>)OL/, 'makeOrderedList' );
keyHandlers[ ctrlKey + '[' ] =
changeIndentationLevel( 'decreaseQuoteLevel', 'decreaseListLevel' );
keyHandlers[ ctrlKey + ']' ] =
changeIndentationLevel( 'increaseQuoteLevel', 'increaseListLevel' );
keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' );
keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
@@ -4417,6 +4459,12 @@ proto.insertHTML = function ( html, isPaste ) {
this._docWasChanged();
}
range.collapse( false );
// After inserting the fragment, check whether the cursor is inside
// an <a> element and if so if there is an equivalent cursor
// position after the <a> element. If there is, move it there.
moveRangeBoundaryOutOf( range, 'A', root );
this._ensureBottomLine();
}
@@ -4947,4 +4995,4 @@ if ( typeof exports === 'object' ) {
}
}
}( document ) );
}( document ) );
@@ -182,7 +182,6 @@
openNote(){
const noteId = this.item.note_id
this.$router.push('/notes/open/'+noteId)
this.$bus.$emit('open_note', noteId)
},
openEditAttachments(){
const noteId = this.item.note_id
+3 -3
View File
@@ -24,9 +24,9 @@
}
},
beforeMount(){
this.$bus.$on('reset_fast_filters', () => {
this.orderString = 'Order by Last Edited'
})
// this.$bus.$on('reset_fast_filters', () => {
// this.orderString = 'Order by Last Edited'
// })
},
methods:{
displayString(){
+83 -63
View File
@@ -19,9 +19,10 @@
bottom: 0;
}
.menu-logo-display {
width: 25px;
margin: 5px 0 0 42px;
width: 27px;
margin: 5px 0 0 41px;
display: inline-block;
height: auto;
}
.menu-item {
@@ -79,15 +80,17 @@
background-color: var(--small_element_bg_color);
border-bottom: 1px solid;
border-color: var(--border_color);
padding: 5px 1rem 5px;
/*padding: 5px 1rem 5px;*/
display: flex;
justify-content: space-around;
}
.place-holder {
width: 100%;
height: 50px;
}
.top-menu-bar img {
width: 30px;
height: 30px;
.logo-display {
width: 27px;
height: auto;
}
.version-display {
position: absolute;
@@ -101,6 +104,19 @@
cursor: pointer;
}
.mobile-button {
display: inline-block;
font-size: 2em;
padding: 6px 3px 5px;
cursor: pointer;
}
.mobile-button.active {
background-color: transparent;
}
.mobile-button i {
margin: 0;
}
</style>
<template>
@@ -110,45 +126,37 @@
<!-- collapsed menu, appears as a bar -->
<div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen">
<div class="ui grid">
<div class="seven wide column">
<div class="ui large basic compact icon button" v-on:click="collapseMenu">
<i class="green bars icon"></i>
</div>
<!-- <router-link v-if="loggedIn" class="ui large basic compact icon button" to="/notes" v-on:click.native="emitReloadEvent()">
<i class="green home icon"></i>
</router-link> -->
<router-link v-if="loggedIn" class="ui basic icon button" exact-active-class="active" to="/attachments">
<i class="open folder outline icon"></i>
</router-link>
</div>
<div class="two wide center aligned bottom aligned column">
<img loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
</div>
<div class="seven wide right aligned column">
<div v-on:click="toggleNightMode" class="ui large basic compact icon button">
<i class="green moon outline icon"></i>
</div>
<!-- mobile create note button -->
<span v-if="loggedIn">
<span v-if="!disableNewNote" @click="createNote" class="ui large green compact icon button">
<i class="plus icon"></i>
</span>
<span v-if="disableNewNote" class="ui large basic compact icon button">
<i class="grey plus icon"></i>
</span>
</span>
</div>
<div class="mobile-button">
<i class="green link bars icon" v-on:click="collapseMenu"></i>
</div>
<div class="mobile-button"></div>
<router-link v-if="loggedIn" to="/quick" class="mobile-button" exact-active-class="active">
<i class="green sticky note outline icon"></i>
</router-link>
<router-link v-if="loggedIn" class="mobile-button" exact-active-class="active" to="/notes" v-on:click.native="emitReloadEvent()">
<logo class="logo-display" color="var(--main-accent)" />
</router-link>
<router-link v-if="loggedIn" class="mobile-button" exact-active-class="active" to="/attachments">
<i class="green open folder outline icon"></i>
</router-link>
<div class="mobile-button"></div>
<!-- mobile create note button -->
<span v-if="loggedIn">
<span v-if="!disableNewNote" @click="createNote" class="mobile-button">
<i class="green plus icon"></i>
</span>
<span v-if="disableNewNote" class="mobile-button">
<i class="grey plus icon"></i>
</span>
</span>
</div>
<div class="shade" v-if="mobile && !collapsed" v-on:click="collapseMenu"></div>
@@ -161,7 +169,7 @@
<div class="menu-section" v-on:click="collapseMenu">
<!-- <div class="menu-item menu-button" > -->
<i class="white angle left icon"></i>
<img class="menu-logo-display" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
<logo class="menu-logo-display" color="var(--main-accent)" />
<!-- </div> -->
</div>
@@ -236,14 +244,14 @@
<div class="menu-section">
<router-link class="menu-item menu-button" exact-active-class="active" to="/help">
<i class="question circle outline icon"></i>Help
<i class="question circle outline icon"></i>Help / Terms
</router-link>
</div>
<div class="menu-section" v-if="loggedIn" :data-tooltip="`Logout ${this.$store.getters.getUsername}`" data-inverted="" data-position="right center">
<div v-on:click="destroyLoginToken" class="menu-item menu-button">
<i v-if="userIcon" class="user outline icon"></i>{{ usernameDisplay }}
</div>
<div class="menu-section" v-if="loggedIn" :data-tooltip="`Settings for ${this.$store.getters.getUsername}`" data-inverted="" data-position="right center">
<router-link class="menu-item menu-button" exact-active-class="active" to="/settings">
<i v-if="userIcon" class="cog icon"></i>{{ usernameDisplay }}
</router-link>
</div>
@@ -264,6 +272,7 @@
components: {
'search-input': require('@/components/SearchInput.vue').default,
'counter':require('@/components/AnimatedCounterComponent.vue').default,
'logo':require('@/components/LogoComponent.vue').default,
},
data: function(){
return {
@@ -274,10 +283,14 @@
disableNewNote: false,
menuOpen: true,
userIcon: true,
resizeDebounce: null,
}
},
beforeCreate: function(){
beforeMount(){
window.addEventListener('resize', this.resizeEventHandler)
},
beforeDestroy(){
window.removeEventListener('resize', this.resizeEventHandler)
},
mounted: function(){
this.mobile = this.$store.getters.getIsUserOnMobile
@@ -316,6 +329,19 @@
},
},
methods: {
resizeEventHandler(e) {
clearTimeout(this.resizeDebounce)
this.resizeDebounce = setTimeout(() => {
this.menuOpen = false
this.collapsed = false
if(window.innerWidth < 700){
this.collapsed = true
}
}, 100)
},
menuClicked(){
//Collapse menu when item is clicked in mobile
if(this.mobile && !this.collapsed){
@@ -334,28 +360,22 @@
},
createNote(event){
const title = ''
this.disableNewNote = true
axios.post('/api/note/create', {title})
axios.post('/api/note/create', {title:''})
.then(response => {
if(response.data && response.data.id){
//Redirect to note page if user is not on it
this.$bus.$emit('open_note', response.data.id)
//Push new note to url and it will open
this.$router.push('/notes/open/'+response.data.id)
this.disableNewNote = false
}
})
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') })
},
destroyLoginToken() {
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')
},
@@ -1,7 +1,7 @@
<template>
<div class="loading-container">
<svg version="1.1" id="L6" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
<rect fill="none" :stroke="$store.getters.getIsNightMode > 0 ? '#FFF':'#16ab39'" stroke-width="4" x="25" y="25" width="50" height="50" rx="5">
<rect fill="none" :stroke="$store.getters.getIsNightMode > 0 ? '#FFF':'var(--main-accent)'" stroke-width="4" x="25" y="25" width="50" height="50" rx="5">
<animateTransform
attributeName="transform"
dur="0.5s"
@@ -12,7 +12,7 @@
attributeType="XML"
begin="rectBox.end"/>
</rect>
<rect x="25" y="25" :fill="$store.getters.getIsNightMode > 0 ? '#FFF':'#16ab39'" width="50" height="50">
<rect x="25" y="25" :fill="$store.getters.getIsNightMode > 0 ? '#FFF':'var(--main-accent)'" width="50" height="50">
<animate
attributeName="height"
dur="1.3s"
+64 -11
View File
@@ -1,3 +1,4 @@
<template>
<div v-on:keyup.enter="login()">
@@ -14,6 +15,11 @@
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</div>
<div class="field" v-if="require2FA">
<div class="ui input">
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
</div>
</div>
<div class="sixteen wide field">
<div class="ui fluid buttons">
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui green button">
@@ -27,34 +33,54 @@
</div>
</div>
</div>
<div class="sixteen wide column">
<span class="small-terms">
By signing up you agree to Solid Scribe's
<router-link to="/help">
Terms of Use
</router-link>
</span>
</div>
</div>
<!-- Thin form display -->
<div v-if="thin" class="ui small form">
<div class="fields">
<div class="four wide field">
<div class="equal width fields">
<div class="field">
<div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
</div>
</div>
<div class="four wide field">
<div class="field">
<div class="ui input">
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</div>
<div class="four wide field">
<div class="field" v-if="require2FA">
<div class="ui input">
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
</div>
</div>
<!-- hide this field if someone is logging in with 2FA -->
<div class="field" v-if="!require2FA">
<div v-on:click="register()" class="ui fluid green button">
<i class="plug icon"></i>
Sign Up
</div>
</div>
<div class="four wide field">
<div class="field">
<div v-on:click="login()" class="ui fluid button">
<i class="power icon"></i>
Login
</div>
</div>
</div>
<span class="small-terms">
By signing up you agree to Solid Scribe's
<router-link to="/help">
Terms of Use
</router-link>
</span>
</div>
@@ -83,7 +109,9 @@
return {
enabled: false,
username: '',
password: ''
password: '',
authToken: '',
require2FA: false,
}
},
methods: {
@@ -139,14 +167,30 @@
return
}
axios.post('/api/user/login', {'username': this.username, 'password': this.password})
axios.post('/api/user/login', {'username': this.username, 'password': this.password, 'authToken':this.authToken })
.then(({data}) => {
if(data == false){
this.$bus.$emit('notification', 'Unable to Login - Incorrect Username or Password')
//Enable 2FA on form
if(data.success == false && data.verificationRequired == true && this.require2FA == false){
this.$bus.$emit('notification', data.message)
this.require2FA = true
this.$nextTick(() => {
this.$refs.authForm.focus()
})
return
}
this.finalizeLogin(data)
if(data.success == false){
this.$bus.$emit('notification', data.message)
return
}
if(data.success){
this.finalizeLogin(data)
return
}
})
.catch(error => {
this.$bus.$emit('notification', 'Unable to Login - Incorrect Username or Password')
@@ -154,4 +198,13 @@
}
}
}
</script>
</script>
<style type="text/css" scoped="true">
.small-terms {
display: inline-block;
text-align: right;
width: 100%;
font-size: 0.9em;
}
</style>
+80
View File
@@ -0,0 +1,80 @@
<template>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 132.29166 132.29167"
height="500"
width="500">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
style="display:inline"
transform="translate(0,-164.70832)"
id="layer1">
<path
class="darken-accent"
id="path3813-4"
d="m 56.22733,165.36641 -55.56249926,15.875 8e-7,63.5 47.62499846,11.90625 v 27.78125 l -47.76066333,-13.9757 0.13566407,10.00695 55.56249926,15.875 v -47.625 l -47.6249985,-11.90625 -8e-7,-47.625 47.7606633,-13.94121 c 0.135664,-2.30629 -0.135664,-9.87129 -0.135664,-9.87129 z"
:style="`fill:${displayColor};fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1`" />
<path
class="brighten-accent"
id="path4563"
d="m 20.508581,220.92891 c 15.265814,-14.23899 27.809717,-7.68002 39.687499,3.96875 v -7.9375 C 51.75093,200.8366 37.512584,206.01499 20.508581,205.05391 Z"
:style="`fill:${displayColor};fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1`" />
<path
class="brighten-accent"
id="path4563-6"
d="m 111.78985,220.92891 c -15.265834,-14.23899 -27.809737,-7.68002 -39.68752,3.96875 v -7.9375 c 8.445151,-16.12356 22.683497,-10.94517 39.68752,-11.90625 z"
:style="`display:inline;fill:${displayColor};fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1`" />
<path
class="brighten-accent"
id="path3813-4-2"
d="m 76.07108,165.36641 55.5625,15.875 v 63.5 l -47.625,11.90625 v 27.78125 l 47.76067,-13.9757 -0.13567,10.00695 -55.5625,15.875 v -47.625 l 47.625,-11.90626 V 189.17891 L 75.93542,175.2377 c -0.13567,-2.30629 0.13566,-9.87129 0.13566,-9.87129 z"
:style="`display:inline;fill:${displayColor};fill-opacity:1;stroke:none;stroke-width:0.52916676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1`" />
</g>
</svg>
</template>
<script>
export default {
name: 'LoadingIcon',
props:[ 'color' ],
data(){
return {
displayColor: '#21BA45', //Default green color
}
},
created(){
//Set color if passed
if(this.color){
this.displayColor = this.color
}
},
}
</script>
<style type="text/css" scoped>
.darken-accent {
filter: brightness(62%);
}
.brighten-accent {
filter: saturate(145%);
}
</style>
+92 -46
View File
@@ -2,9 +2,8 @@
<!-- change class to .master-note-edit to have it popup on the screen -->
<div
id="InputNotes"
class="master-note-edit full-focus"
@keyup.esc="close()"
:class="[ 'position-'+position ]"
class="master-note-edit full-focus position-0"
@keyup.esc="closeButtonAction()"
>
<!-- Giant Edit Note Menu -->
@@ -14,7 +13,7 @@
<div class="edit-spacer"></div>
<div class="menu-top-half">
<div class="edit-button" v-on:click="close()" data-tooltip="Close" data-position="bottom center" data-inverted>
<div class="edit-button" v-on:click="closeButtonAction()" data-tooltip="Close" data-position="bottom center" data-inverted>
<i class="close icon"></i>
</div>
@@ -61,7 +60,19 @@
</div>
<div class="menu-bottom-half">
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/table`)" data-tooltip="Insert Table" data-position="bottom center" data-inverted>
<i class="border all icon"></i>
</div>
<div class="edit-button" v-on:click="insertDivide()" data-tooltip="Insert Divide" data-position="bottom center" data-inverted>
<i class="grip lines icon"></i>
</div>
<div class="edit-button" v-on:click="insertTable(4,4)" data-tooltip="Insert Table" data-position="bottom center" data-inverted>
<i class="book dead icon"></i>
</div>
<div class="edit-button" v-on:click="removeFormatting()" data-tooltip="Remove Formatting" data-position="bottom center" data-inverted>
<i class="remove format icon"></i>
</div>
@@ -95,7 +106,8 @@
<div class="edit-divide"></div>
<!-- <div class="edit-button" v-on:click="onToggleArchived()" :data-tooltip="archived == 1?'Move to main list':'Move to Archive'" data-position="bottom center" data-inverted>
<!--
<div class="edit-button" v-on:click="onToggleArchived()" :data-tooltip="archived == 1?'Move to main list':'Move to Archive'" data-position="bottom center" data-inverted>
<span v-if="archived == 1"><i class="green archive icon"></i></span>
<span v-if="archived != 1"><i class="archive icon"></i></span>
</div>
@@ -105,17 +117,13 @@
<span v-if="pinned != 1"><i class="pin icon"></i></span>
</div> -->
<!-- data-tooltip="Files on note" -->
<!-- <div v-if="attachmentCount > 0" class="edit-button" v-on:click="openEditAttachment" data-tooltip="Files" data-position="bottom center" data-inverted>
<i class="folder icon"></i>
{{ attachmentCount }}
</div> -->
<div class="edit-button" v-if="usersOnNote > 1">
<i class="green eye icon"></i> {{ usersOnNote }}
</div>
<!-- <div class="edit-button" v-on:click="simulateTyping()">
<!--
<div class="edit-button" v-on:click="simulateTyping()">
<i class="purple bolt icon"></i>
</div> -->
@@ -132,7 +140,6 @@
</div>
</div>
</div>
<div class="bottom-edit-menu"></div>
@@ -163,7 +170,7 @@
</textarea>
<!-- Squire Box -->
<div id="squire-id" class="squire-box" ref="squirebox" placeholder="Note Text"></div>
<div id="squire-id" class="squire-box" ref="squirebox" placeholder="Type Note Here"></div>
</div>
@@ -224,19 +231,19 @@
<div class="sixteen wide column">
<div class="ui labeled icon fluid basic button" v-on:click="sortList">
<i class="sort amount up icon"></i>
Sort List items (Move checked to bottom)
Sort List
</div>
</div>
<div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="deleteCompletedListItems">
<i class="trash icon"></i>
Delete Checked Items
Delete Checked
</div>
</div>
<div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="uncheckAllListItems">
<i class="list ul icon"></i>
Uncheck all Checked items
Uncheck All
</div>
</div>
<div class="eight wide column">
@@ -245,6 +252,15 @@
Simple Math
</div>
</div>
<div class="eight wide column">
<!-- data-tooltip="Files on note" -->
<div v-on:click="openEditAttachment" class="ui labeled icon fluid basic button">
<i class="folder icon"></i>
Note Files
{{ attachmentCount }}
</div>
</div>
<div class="sixteen wide column" v-if="rawTextId > 0">
<h2>Share Note</h2>
<share-note-component
@@ -257,13 +273,20 @@
</div>
</side-slide-menu>
<!-- create table option -->
<side-slide-menu v-if="table" v-on:close="table = false; fetchNoteTags()" name="table" :style-object="styleObject">
<div class="ui basic segment">
Create a table
</div>
</side-slide-menu>
<!-- Show side shades if user is on desktop only -->
<div class="full-focus-shade shade1"
:class="{ 'slide-out-left':sizeDown }"
v-on:click="close()"></div>
v-on:click="closeButtonAction()"></div>
<div class="full-focus-shade shade2"
:class="{ 'slide-out-right':sizeDown }"
v-on:click="close()"></div>
v-on:click="closeButtonAction()"></div>
</div>
</template>
@@ -335,6 +358,7 @@
images: false,
options: false,
colorpicker: false,
table: false,
//Diff text/sync text variables
diffTextTimeout: null,
@@ -355,7 +379,7 @@
//Handle changes in URL to
if(newVal.id == undefined || newVal.id != this.noteid){
this.close()
// this.closeButtonAction()
}
//Reset all note menus on URL change
@@ -364,6 +388,7 @@
this.tags = false
this.options = false
this.images = false
this.table = false
//If a menu value is set, open it
if(newVal.openMenu){
@@ -394,18 +419,20 @@
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
// if(this.editor){
this.editor.destroy()
// }
//Obliterate squire instance
this.editor.destroy()
this.close()
},
mounted: function() {
//Show loading for a minimum time
setTimeout(()=>{
this.forceShowLoading = false
}, 500)
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
// document.addEventListener('visibilitychange', this.checkForUpdatedNote)
//Init squire as early as possible
this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' })
@@ -599,6 +626,30 @@
this.editor.addEventListener('keydown', event => {
//Tab to increase quote level, tab + shigt to decrease quote level
const keyCode = event.key
if(keyCode == 'Tab'){
if(event.shiftKey){
this.editor.decreaseQuoteLevel()
} else {
this.editor.increaseQuoteLevel()
}
event.preventDefault()
return false
}
//Save on pressing CTRL/CMD + S
if(keyCode == 's' && (event.ctrlKey || event.metaKey) ){
this.$bus.$emit('notification', 'Note Saved')
this.save()
event.preventDefault()
return false
}
//Prevent new list items from having
this.$nextTick( () => {
//Wait a moment to get item under cursor
@@ -612,10 +663,6 @@
})
})
this.editor.addEventListener('keydown', event => {
})
//Bind event handlers
this.editor.addEventListener('keyup', event => {
@@ -682,7 +729,7 @@
//Block notes you don't have access to from opening
if(response.data === false){
this.$bus.$emit('notification', 'Error opening Note')
this.close(true)
this.close()
return
}
@@ -915,18 +962,21 @@
return hash;
},
close(force = false){
closeButtonAction(){
this.sizeDown = true
//This timeout allows animation to play before closing
setTimeout(() => {
this.$router.push('/notes')
}, 300)
},
close(){
// force = true
// console.log(`Close Note ${this.noteid} -> force: ${force}, modified: ${this.modified}`)
if(force){
this.$bus.$emit('close_active_note', {
noteId: this.noteid, modified: this.modified
})
return
}
//Skip everything if foce close is true. Note will just die.
if(this.currentNoteId == 0){ return }
this.loadingMessage = 'Saving...'
this.loading = true
@@ -938,14 +988,10 @@
axios.post('/api/note/reindex')
}
this.sizeDown = true
//This timeout allows animation to play before closing
setTimeout(() => {
this.$bus.$emit('close_active_note', {
noteId: this.noteid, modified: this.modified
})
return
}, 300)
this.$bus.$emit('close_active_note', {
noteId: this.noteid, modified: this.modified
})
return
})
},
destroyWebSockets(){
@@ -1005,8 +1051,8 @@
let element = this.$refs.titleTextarea
let padding = 0
element.style.height = 'auto';
element.style.height = (element.scrollHeight + padding) +'px';
element.style.height = 'auto'
element.style.height = (element.scrollHeight + padding) +'px'
},
}
}
@@ -1152,7 +1198,7 @@
background-color: var(--menu-accent);
}
.edit-active {
background-color: #21BA45;
background-color: var(--main-accent);
color: white;
}
.edit-divide {
+51 -27
View File
@@ -67,7 +67,7 @@
</span>
<span class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
{{$helpers.timeAgo(note.updated)}}
{{$helpers.timeAgo( note.updated )}}
</span>
<span class="teeny-buttons" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
@@ -234,12 +234,12 @@
},
justClosed(){
//Scroll note into view
// this.$el.scrollIntoView({
// behavior: 'smooth',
// block: 'center',
// inline: 'center'
// })
// Scroll note into view
this.$el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
//After scroll, trigger green outline animation
setTimeout(() => {
@@ -356,13 +356,14 @@
/*Strict font sizes for card display*/
.small-text {
max-height: 261px;
max-height: 267px;
width: 100%;
overflow: hidden;
display: inline-block;
}
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
/*font-size: 1.0em !important;*/
font-size: 16px !important;
font-size: 14px !important;
}
.small-text > p, , .small-text > h1, .small-text > h2 {
margin-bottom: 0.5em;
@@ -370,7 +371,7 @@
.big-text > p:first-child,
.big-text > h1, .big-text > h2 {
/*font-size: 1.3em !important;*/
font-size: 17px !important;
font-size: 20px !important;
font-weight: bold;
margin-bottom: 0.5em;
}
@@ -415,14 +416,14 @@
border-color: var(--border_color);
/*width: calc(33.333% - 10px);*/
width: calc(25% - 10px);
min-width: 190px;
/*min-width: 190px;*/
min-height: 130px;
/*transition: box-shadow 0.3s;*/
box-sizing: border-box;
cursor: pointer;
line-height: 1.8rem;
letter-spacing: 0.02rem;
letter-spacing: 0.05rem;
display: flex;
flex-direction: column;
text-align: left;
@@ -555,27 +556,50 @@
float: right;
}
/* Tweak mobile display to show only one column */
@media only screen and (min-width: 1500px) {
.note-title-display-card {
width: calc(20% - 10px);
}
}
@media only screen and (max-width: 740px) {
/* Break points determine when display cards shrink */
@media only screen and (max-width: 700px) {
.note-title-display-card {
width: calc(100% + 10px);
margin: 0px -5px 10px -5px;
}
}
@media only screen and (min-width: 700px) and (max-width: 900px) {
.note-title-display-card {
width: calc(50% - 10px);
}
}
@media only screen and (min-width: 900px) and (max-width: 1100px) {
.note-title-display-card {
width: calc(33.33333% - 10px);
}
}
@media only screen and (min-width: 1100px) and (max-width: 1300px) {
.note-title-display-card {
width: calc(25% - 10px);
}
}
@media only screen and (min-width: 1300px) and (max-width: 1800px) {
.note-title-display-card {
width: calc(20% - 10px);
}
}
@media only screen and (min-width: 1800px) {
.note-title-display-card {
width: calc(16.66666% - 10px);
}
}
/*Animations for cool border effects*/
@keyframes bgin {
0% {
background-image:
linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* TopLeft to Right */
linear-gradient(to bottom, #21BA45 50%, #21BA45 100%), /* TopRight to Bottom */
linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* BottomLeft to Right*/
linear-gradient(to bottom, #21BA45 50%, #21BA45 100%); /* TopLeft to Bottom */
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */
/*Initial state, no BG*/
background-size: 0 4px, 4px 0, 0 4px, 4px 0;
}
@@ -590,10 +614,10 @@
30% {
background-size: 100% 4px, 4px 100%, 100% 4px, 4px 100%;
background-image:
linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* TopLeft to Right */
linear-gradient(to bottom, #21BA45 50%, #21BA45 100%), /* TopRight to Bottom */
linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* BottomLeft to Right*/
linear-gradient(to bottom, #21BA45 50%, #21BA45 100%); /* TopLeft to Bottom */
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */
}
100% {
background-image:
+6 -3
View File
@@ -98,13 +98,17 @@
},
beforeCreate: function(){
},
mounted: function(){
beforeMount(){
//search clear
this.$bus.$on('reset_fast_filters', () => {
this.searchTerm = ''
this.tagSuggestions = []
})
},
beforeDestroy(){
this.$bus.$off('reset_fast_filters')
},
mounted: function(){
},
methods: {
@@ -148,7 +152,6 @@
if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id)
}
})
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') })
+84 -20
View File
@@ -17,6 +17,7 @@ const SquireButtonFunctions = {
//
pathChangeEvent(e){
//Reset all button states
this.activeBold = false
this.activeTitle = false
@@ -143,28 +144,23 @@ const SquireButtonFunctions = {
// Uncheck All List Items
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container
let container = document.getElementById('squire-id')
Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
deleteCompletedListItems(){
//
// Delete Completed List Items
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container
let container = document.getElementById('squire-id')
@@ -210,17 +206,17 @@ const SquireButtonFunctions = {
}
})
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
sortList(){
//
// Sort list, checked at the bottom, unchecked at the top
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container
let container = document.getElementById('squire-id')
@@ -274,17 +270,17 @@ const SquireButtonFunctions = {
}
})
//Close menu if user is on mobile
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
calculateMath(){
//
// Find math in note and calculate the outcome
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.options = false
}
//Fetch the container
let container = document.getElementById('squire-id')
@@ -327,6 +323,11 @@ const SquireButtonFunctions = {
}
})
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
setText(inText){
@@ -338,6 +339,69 @@ const SquireButtonFunctions = {
return this.editor.getHTML()
},
insertDivide(){
this.editor.insertHTML(`<p><div class='divide'></div><br></p>`)
this.editor.focus()
this.editor.moveCursorToEnd()
},
insertTable(wide, tall){
console.log('Insert a table')
let tableSyntax = `
<div>
<table>
<tr>
<th><p><br></p></th>
<th><p><br></p></th>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
</table>
</div>
`
tableSyntax = `
<span class="t-table">
<span>
<span>
<p><br></p>
</span>
<span>
<p><br></p>
</span>
</span>
<span>
<span>
<p><br></p>
</span>
<span>
<p><br></p>
</span>
</span>
</span>
<p><br></p>
`
tableSyntax = ''
tableSyntax += '<span class="t-table">'
for (let i = 0; i < tall; i++) {
for (let j = 0; j < wide; j++) {
}
}
tableSyntax += '</span><p><br></p>'
this.editor.insertHTML(tableSyntax)
this.editor.focus()
this.editor.moveCursorToEnd()
},
},
}
+18 -12
View File
@@ -1,24 +1,30 @@
<template>
<div class="ui basic segment">
<div class="ui container">
<div class="ui grid">
<div class="sixteen wide column">
<div class="ui text container">
<div class="ui raised segment">
<div class="sixteen wide column"></div>
<div class="sixteen wide column squire-box">
<h2 class="ui dividing header">
<i class="green question circle outline icon"></i>
Help
</h2>
<!-- Content copied from note -->
<!-- https://www.solidscribe.com/#/notes/open/552 -->
<div class="ui segment">
<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 -->
<!-- https://www.solidscribe.com/#/notes/open/552 -->
<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>
<!-- content copied from note -->
</div>
</div>
<h2 class="ui dividing header">
Solid Scribe Terms of Use
</h2>
<div class="ui segment">
<p>Welcome to Solid Scribe’s web-based application at solidscribe.com.<br></p><p><br></p><p>Your use of the Site and/or participation in any of the Services offered through the Site, is subject to these Terms of Use.<br></p><p>By registering an an account or using the Site you agree to be bound by these Terms of Use.<br></p><p><br></p><p>The Company reserves the right to change these Terms of Use from time to time by publishing them on the Site.<br></p><p>By using the Site, you agree to the most current version of these Terms of Use as posted on the Site.<br></p><p><br></p><p>All content on the Site is provided "as is." Solid Scribe is not liable for any informational errors, incompleteness, delays or any actions taken in reliance on information contained on the Site. The Company cannot and does not guarantee that any content on the Site will be entirely accurate or up-to-date at all times as the information on the Site originates from various sources, which may or may not be within the control of the Company.<br></p><p><br></p><p>The Company is not responsible for any omission, deletion, failure to store, improper delivery or untimely delivery of any information or material. The Company is not responsible for any reliance upon or damage arising from the use of the Site, including technical damage resulting from downloading or accessing any information or material provided.<br></p><p><br></p><p>You understand and agree that the Company is not responsible for any content posted by you or third parties. You further understand that the Company does not monitor the content posted by other users.<br></p><p><br></p><p>The Company reserves the right to remove content that the Company deems inappropriate for any reason what so ever without notice. The Company also reserves the right, in the Company’s sole discretion, to remove a user's privilege to post content on the Site.<br></p><p><br></p><p>The Company does not in any manner endorse the content of its users and cannot and does not vouch for its reliability or otherwise accept liability for it.<br></p><p><br></p><p>Minors (persons under 18 years of age), persons or organizations that cannot enter into binding contracts under applicable law, and those who may be prevented by contractual obligations from entering into binding contracts with Solid Scribe are not allowed to use this Site.<br></p><p><br></p><p>The Company has the right, but not the obligation, to take any of the following actions in the Company’s sole discretion at any time and for any reason without providing you any prior notice:<br></p><p><br></p><p>1. Restrict, suspend, modify or terminate access to the Site or all or any part of the Company’s Services;<br></p><p><br></p><p>2. Change, suspend or discontinue all or any part of the Company’s Services;<br></p><p><br></p><p>3. Refuse, move, modify or remove any material submitted to the Site by any person, for any reason;<br></p><p><br></p><p>4. Refuse, move, modify or remove any content available on the Site;<br></p><p><br></p><p>5. Deactivate or delete your account and all related information and files in your account;<br></p><p><br></p><p>6. Establish, discontinue or change general practices and limits concerning use of the Site and the Company’s Services.<br></p><p><br></p><p>You agree that the Company is not liable to you or any third party for taking any of these actions. Any registration or other eligibility requirements are in the sole discretion of the Company and may be modified for any reason at any time.<br></p><p><br></p><p>YOUR INFORMATION AND CONTENT<br></p><p><br></p><p>Any information you provide to Solid Scribe or other users during your registration or use of the Site (in any public message area, through any mail feature, or otherwise generated out of your use of the Site or the Services), is subject to the following:<br></p><p><br></p><p>(a) You are solely responsible for the content of your information, postings or Notes. The Company reserves the right to take any action with respect to such information that it deems necessary or appropriate in its sole discretion for any reason, including if the Company believes information is inappropriate for the Site or the Services, may create liability for the Company.<br></p><p><br></p><p>(b) You agree that your information will not, to the best of your knowledge: (i) violate any law (including without limitation those governing export control, consumer protection, unfair competition, anti-discrimination or false advertising); or (ii) be libelous, defamatory, obscene, abusive, violate a third party's right to privacy, otherwise violate any applicable local, state, national or international law, amount to spamming or be otherwise inappropriate. You may not post content that degrades others on the basis of gender, race, class, ethnicity, national origin, religion, sexual preference, disability or other classification. Epithets and other language intended to intimidate, unlaLine truncated
</div>
</div>
</div>
</div>
+25 -14
View File
@@ -110,7 +110,7 @@
</h2>
<h3 class="subtext">
A free, secure Note App<i class="i cursor icon blinking"></i>
A free, secure, online note taking application<i class="i cursor icon blinking"></i>
</h3>
</div>
@@ -210,18 +210,7 @@
<i class="bottom left corner blue pen icon"></i>
</i>
Document Editing Tools
<div class="sub header">Bold, Underline, Title, Add Links, Add Tables Color Text, Color Background and more.</div>
</div>
</h2>
<h2 class="ui dividing header">
<div class="content">
<i class="icons">
<i class="grey tags icon"></i>
<i class="bottom left corner purple plus icon"></i>
</i>
Tag Notes
<div class="sub header">Easily add and edit tags on notes then sort notes by tag.</div>
<div class="sub header">Bold, Underline, Title, Add Links, Add Tables, Color Text, Color Background and more.</div>
</div>
</h2>
@@ -236,6 +225,17 @@
</div>
</h2>
<h2 class="ui dividing header">
<div class="content">
<i class="icons">
<i class="grey tags icon"></i>
<i class="bottom left corner purple plus icon"></i>
</i>
Tag Notes
<div class="sub header">Easily add and edit tags on notes then sort notes by tag.</div>
</div>
</h2>
<h2 class="ui dividing header">
<div class="content">
<i class="icons">
@@ -254,7 +254,7 @@
<i class="bottom left corner share icon"></i>
</i>
Share Encrypted Notes
<div class="sub header">Share notes with friends without compromising security. And its easy to disable sharing.</div>
<div class="sub header">Share encrypted notes with friends without compromising security.</div>
</div>
</h2>
@@ -290,6 +290,17 @@
<div class="sub header">Add text to Images and links than can be searched.</div>
</div>
</h2>
<h2 class="ui dividing header">
<div class="content">
<i class="icons">
<i class="grey tv icon"></i>
<i class="bottom left corner blue mobile icon"></i>
</i>
Two Factor Authentication
<div class="sub header">Enable two factor authentication for added peace of mind.</div>
</div>
</h2>
</div>
<div class="four wide column">
+1 -1
View File
@@ -25,7 +25,7 @@
</div>
<p>You will remain logged in on this browser, for 30 days or until you log out.</p>
<p>You will remain logged in on this browser, for 20 days or until you log out.</p>
</div>
</div>
</div>
+49 -78
View File
@@ -31,7 +31,7 @@
<i v-if="titleView" class="th icon"></i>
<i v-if="!titleView" class="bars icon"></i>
</div>
</div>
<div class="eight wide column" v-if="showClear">
@@ -117,7 +117,7 @@
:onClick="openNote"
:data="note"
:title-view="titleView"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:currently-open="activeNoteId1 == note.id"
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
/>
</div>
@@ -132,13 +132,12 @@
</div>
<input-notes
<note-input-panel
v-if="activeNoteId1 != null"
:key="'active_note_'+activeNoteId1"
:key="activeNoteId1"
:noteid="activeNoteId1"
:position="activeNote1Position"
:url-data="$route.params"
ref="note1" />
/>
</div>
</template>
@@ -151,7 +150,7 @@
name: 'SearchBar',
components: {
'input-notes': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
// 'fast-filters': require('@/components/FastFilters.vue').default,
@@ -247,7 +246,7 @@
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.updateSingleNote(noteId, false)
this.updateSingleNote(noteId, true)
}
})
@@ -264,10 +263,18 @@
//Close note event
this.$bus.$on('close_active_note', ({noteId, modified}) => {
this.closeNote()
if(modified){
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
}
//A note has been closed
if(this.$route.fullPath != '/notes'){
this.$router.push('/notes')
}
this.$store.dispatch('fetchAndUpdateUserTotals')
//Focus and animate if modified
this.updateSingleNote(parseInt(noteId), modified)
this.updateSingleNote(noteId, modified)
})
this.$bus.$on('note_deleted', (noteId) => {
@@ -312,35 +319,25 @@
})
})
//New note button pushes open note event
this.$bus.$on('open_note', noteId => {
this.openNote(noteId)
})
//Reload page content
//Reload page content - don't trigger if load is in progress
this.$bus.$on('note_reload', () => {
this.reset()
if(!this.loadingInProgress){
this.reset()
}
})
//Mount notes on load if note ID is set
if(this.$route.params && this.$route.params.id){
const id = this.$route.params.id
this.openNote(id)
}
window.addEventListener('scroll', this.onScroll)
//Close notes when back button is pressed
window.addEventListener('hashchange', this.hashChangeAction)
// window.addEventListener('hashchange', this.hashChangeAction)
//update note on visibility change
document.addEventListener('visibilitychange', this.visibiltyChangeAction);
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
},
beforeDestroy(){
window.removeEventListener('scroll', this.onScroll)
window.removeEventListener('hashchange', this.hashChangeAction)
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
this.$bus.$off('note_reload')
this.$bus.$off('close_active_note')
@@ -348,7 +345,6 @@
this.$bus.$off('note_deleted')
this.$bus.$off('update_fast_filters')
this.$bus.$off('update_search_term')
this.$bus.$off('open_note')
//We want to remove event listeners, but something here is messing them up and preventing ALL event listeners from working
// this.$off() // Remove all event listeners
@@ -356,11 +352,20 @@
},
mounted() {
//Open note on load if ID is set
if(this.$route.params.id > 1){
this.activeNoteId1 = this.$route.params.id
}
//Loads initial batch and tags
this.reset()
// this.search(true, this.firstLoadBatchSize, false)
// .then( r => this.search(false, this.batchSize, true))
},
watch: {
'$route.params.id': function(id){
//Open note on ID, null id will close note
this.activeNoteId1 = id
}
},
methods: {
toggleTitleView(){
@@ -382,17 +387,9 @@
if(nodeClick == 'A'){ return }
}
//1 note open
if(this.activeNoteId1 == null){
this.activeNoteId1 = id
this.activeNote1Position = 0 //Middel of page
this.$router.push('/notes/open/'+this.activeNoteId1).catch(e => { console.log(e) })
return
}
},
closeNote(position){
this.activeNoteId1 = null
this.$router.push('/notes')
//Open note if a link was not clicked
this.$router.push('/notes/open/'+id)
return
},
toggleTagFilter(tagId){
@@ -429,34 +426,6 @@
return
},
//Try to close notes on URL hash change /notes/open/123 to /notes - parse 123, close note id 123
hashChangeAction(event){
//Clean up path of hash change
let path = window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.hash
let newPath = event.newURL.replace(path,'')
let oldPath = event.oldURL.replace(path,'')
// console.log(this.$route.params)
// console.log(this.$router)
//Open note if user goes forward to a note id
if(this.$route.params && this.$route.params.id){
this.openNote(this.$route.params.id)
}
//If we go from open note ID to no note ID, close the note
if(newPath == '' && oldPath.indexOf('/open/') != -1){
//Pull note ID out of URL
const noteIdToClose = oldPath.split('/').pop()
// console.log(noteIdToClose)
if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){
// this.$refs.note1.close()
}
}
},
visibiltyChangeAction(event){
//Fuck this shit, just use web sockets
@@ -484,8 +453,11 @@
let note = null
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
note = this.$refs['note-'+noteId][0].note
//Show that note is working on updating
this.$refs['note-'+noteId][0].showWorking = true
}
//Lookup one note using passed in ID
const postData = {
searchQuery: this.searchTerm,
@@ -508,18 +480,13 @@
if(note && newNote){
//Don't move notes that were not changed
if(note.updated == newNote.updated){
// return
}
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
//Push new note to front if its modified
if(focuseAndAnimate){
//Push new note to front if its modified or we want it to
if( focuseAndAnimate || note.updated != newNote.updated ){
// Find note, in section, move to front
Object.keys(this.noteSections).forEach( key => {
@@ -536,6 +503,7 @@
this.$nextTick( () => {
//Trigger close animation on note
this.$refs['note-'+noteId][0].justClosed()
this.$refs['note-'+noteId][0].showWorking = false
})
}
@@ -547,9 +515,14 @@
//Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].justClosed()
this.$refs['note-'+noteId][0].showWorking = false
}
}
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].showWorking = false
}
//Trigger section rebuild
this.rebuildNoteCategorise()
})
@@ -610,7 +583,6 @@
//Perform search - or die
this.loadingInProgress = true
// console.time('Fetch TitleCard Batch '+notesInNextLoad)
axios.post('/api/note/search', postData)
.then(response => {
@@ -735,7 +707,6 @@
this.fastFilters = {}
this.foundAttachments = [] //Remove all attachments
this.$bus.$emit('reset_fast_filters')
this.updateFastFilters(5) //This loads notes
},
+5 -4
View File
@@ -14,6 +14,11 @@
<div class="sixteen wide middle aligned column" v-if="quickNoteId > 0">
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui compact basic button">
<i class="file outline icon"></i>
Open Note
</div>
<div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
<i class="sync alternate reload icon"></i>
New Scratch Pad
@@ -46,10 +51,6 @@
<i class="folder open outline icon"></i>
Files
</div>
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">
<i class="file outline icon"></i>
Open Note
</div>
</div>
</div>
</div>
+182
View File
@@ -0,0 +1,182 @@
<template>
<div class="ui grid">
<div class="row"></div>
<!-- spacer column -->
<div class="sixteen wide column">
<h2 class="ui dividing header">
<i class="cog icon"></i>
Settings
</h2>
<!-- Accent Color -->
<div class="ui segment">
<div class="ui grid">
<div class="sixteen wide column">
<h3 class="ui header">
Accent Color
</h3>
<div
v-for="color in themeColors"
class="ui compact basic button"
:style="`background: linear-gradient(0deg, ${color} 4%, rgba(0,0,0,0) 5%);`"
v-on:click="setAccentColor(color)">
<logo style="width: 33px; height: auto;" :color="color" />
</div>
</div>
</div>
</div>
<!-- Enable Two Factor -->
<div class="ui segment">
<h3>Two Factor Authentication</h3>
<div class="ui stackable grid">
<div class="four wide column">
<p>1. Enter Password and get QR</p>
<div class="ui fluid action input">
<input type="password" placeholder="Current Password" v-model="password">
<div v-if="password.length == 0" class="ui disabled button">
Get QR code
</div>
<div v-if="password.length > 0" class="ui green button" v-on:click="getQrCode()">
Get QR code
</div>
</div>
</div>
<div class="five wide column">
<p>2. Scan QR Code</p>
<p v-if="qrCode == ''">QR Code Will appear here.</p>
<img v-if="qrCode != ''" :src="qrCode" alt="QR Code">
</div>
<div class="four wide column">
<p>3. Verify with code</p>
<div class="ui input" v-if="qrCode != ''">
<input type="text" placeholder="Verification Code" v-model="verificationToken" v-on:keyup.enter="verifyQrCode()">
</div>
</div>
</div>
</div>
<!-- change password -->
<div class="ui segment">
<h3>Change Password</h3>
<div class="ui grid">
<div class="five wide column">
<label>Current Password</label>
<div class="ui fluid input">
<input type="text" placeholder="Current Password">
</div>
</div>
<div class="five wide column">
<label>New Password</label>
<div class="ui fluid input">
<input type="text" placeholder="New Password">
</div>
</div>
<div class="six wide column">
<label>Rereat New Password</label>
<div class="ui fluid action input">
<input type="text" placeholder="Repeat Password">
<div class="ui green button">
Change it!
</div>
</div>
</div>
</div>
</div>
<!-- log out -->
<div class="ui segment">
<div class="ui grid">
<div class="sixteen wide column">
<h3>Log Out</h3>
</div>
<div class="eight wide column">
<div class="ui button" v-on:click="logout()">
Log Out this browser
</div>
</div>
<div class="eight wide column">
<div class="ui button">
Log Out all other browsers
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'SettingsPage',
components: {
'logo':require('@/components/LogoComponent.vue').default,
},
data () {
return {
password: '',
qrCode: '',
verificationToken: '',
themeColors: [
'#21BA45', //Green
'#b5cc18', //Lime
'#00b5ad', //Teal
'#2185d0', //Blue
'#7128b9', //Violet
'#a333c8', // "Purple"
'#e03997', //Pink
'#db2828', //Red
'#f2711c', //Orange
'#fbbd08', //Yellow
'#767676', //Grey
'#303030', //Black-almost
]
}
},
methods: {
logout() {
this.$store.commit('destroyLoginToken')
this.$router.push('/')
axios.post('/api/user/logout')
setTimeout(() => {
this.$bus.$emit('notification', 'Logged Out')
}, 200)
},
setAccentColor(color){
let root = document.documentElement
root.style.setProperty('--main-accent', color)
localStorage.setItem('main-accent', color)
if(!color || color == '#21BA45'){
localStorage.removeItem('main-accent')
}
},
getQrCode(){
axios.post('/api/user/twofactorsetup', { password:this.password })
.then(({data}) => {
this.qrCode = data
})
},
verifyQrCode(){
axios.post('/api/user/verifytwofactorsetuptoken', { password:this.password, token: this.verificationToken })
.then(({data}) => {
if(data == true){
//Two FA is set up
} else {
//It failed
}
})
}
}
}
</script>
+7
View File
@@ -6,6 +6,7 @@ import Router from 'vue-router'
const HomePage = () => import(/* webpackChunkName: "HomePage" */ '@/pages/HomePage')
const LoginPage = () => import(/* webpackChunkName: "LoginPage" */ '@/pages/LoginPage')
const HelpPage = () => import(/* webpackChunkName: "HelpPage" */ '@/pages/HelpPage')
const SettingsPage = () => import(/* webpackChunkName: "SettingsPage" */ '@/pages/SettingsPage')
const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/SharePage')
const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage')
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
@@ -51,6 +52,12 @@ export default new Router({
name: 'Help',
meta: {title:'Help'},
component: HelpPage
},
{
path: '/settings',
name: 'Settings',
meta: {title:'Settings'},
component: SettingsPage
},
{
path: '/public/note/:id/:token',
+1
View File
@@ -85,6 +85,7 @@ export default new Vuex.Store({
Object.keys( themes[currentTheme] ).forEach( attribute => {
root.style.setProperty('--'+attribute, themes[currentTheme][attribute])
})
},
detectIsUserOnMobile(state){
+265
View File
@@ -49,6 +49,19 @@
"uri-js": "^4.2.2"
}
},
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
@@ -112,11 +125,21 @@
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
},
"base32.js": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz",
"integrity": "sha1-0EVzalex9sE58MffQlGKhOkbsro="
},
"base64-arraybuffer": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
},
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@@ -182,11 +205,39 @@
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz",
"integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA=="
},
"buffer": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
"integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
},
"buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"requires": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
}
},
"buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@@ -229,6 +280,11 @@
"resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
"integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"camelize": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
@@ -252,6 +308,29 @@
"parse5": "^3.0.1"
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -394,6 +473,11 @@
"ms": "2.0.0"
}
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -441,6 +525,11 @@
}
}
},
"dijkstrajs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz",
"integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs="
},
"dns-prefetch-control": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz",
@@ -509,6 +598,11 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -708,6 +802,14 @@
"unpipe": "~1.0.0"
}
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"requires": {
"locate-path": "^3.0.0"
}
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -746,6 +848,11 @@
"is-property": "^1.0.2"
}
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@@ -926,6 +1033,11 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
@@ -941,6 +1053,11 @@
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
"integrity": "sha1-N9905DCg5HVQ/lSi3v4w2KzZX2U="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
@@ -1040,6 +1157,15 @@
"safe-buffer": "^5.0.1"
}
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
@@ -1257,6 +1383,27 @@
"ee-first": "1.1.1"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"requires": {
"p-limit": "^2.0.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"parse5": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
@@ -1286,6 +1433,11 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@@ -1296,6 +1448,11 @@
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -1325,6 +1482,27 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew="
},
"qrcode": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
"integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==",
"requires": {
"buffer": "^5.4.3",
"buffer-alloc": "^1.2.0",
"buffer-from": "^1.1.1",
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
"yargs": "^13.2.4"
},
"dependencies": {
"isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
}
}
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
@@ -1407,6 +1585,16 @@
"lodash": "^4.17.15"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -1465,6 +1653,11 @@
"send": "0.17.1"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
@@ -1599,6 +1792,14 @@
}
}
},
"speakeasy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz",
"integrity": "sha1-hckaBxsJpcuGQlkNmDVmFl9XYTo=",
"requires": {
"base32.js": "0.0.1"
}
},
"sqlstring": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz",
@@ -1635,6 +1836,16 @@
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"string_decoder": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
@@ -1643,6 +1854,14 @@
"safe-buffer": "~5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"requires": {
"ansi-regex": "^4.1.0"
}
},
"to-array": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
@@ -1755,6 +1974,21 @@
"isexe": "^2.0.0"
}
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"ws": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz",
@@ -1775,11 +2009,42 @@
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q="
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+3 -1
View File
@@ -21,9 +21,11 @@
"multer": "^1.4.2",
"mysql2": "^1.7.0",
"node-tesseract-ocr": "^1.0.0",
"qrcode": "^1.4.4",
"request": "^2.88.2",
"request-promise": "^4.2.5",
"socket.io": "^2.3.0"
"socket.io": "^2.3.0",
"speakeasy": "^2.0.0"
},
"_moduleAliases": {
"@root": ".",
+139
View File
@@ -1,6 +1,7 @@
const db = require('@config/database')
const jwt = require('jsonwebtoken')
const cs = require('@helpers/CryptoString')
const speakeasy = require('speakeasy')
let Auth = {}
@@ -120,6 +121,7 @@ Auth.decodeToken = (token, request = null) => {
}
Auth.terminateSession = (sessionId) => {
return db.promise().query('DELETE from user_active_session WHERE session_id = ?', [sessionId])
}
@@ -130,6 +132,143 @@ Auth.deletAllLoginKeys = (userId) => {
return db.promise().query('DELETE FROM user_active_session WHERE user_hash = ?', [userHash])
}
//Generate two factor secret key, if key is not verified, return a new one
//Only return QR code if user is not verified, only show unique QR code, once
Auth.generateTwoFactorSecretKey = (userId, password) => {
return new Promise((resolve, reject) => {
const QRCode = require('qrcode')
const User = require('@models/User')
User.getMasterKey(userId, password)
.then(masterKey => {
return db.promise().query('SELECT username, two_fa_enabled FROM user WHERE id = ?', [userId])
})
.then((r,f) => {
const tfaEnabled = r[0][0]['two_fa_enabled'] == 1
const username = r[0][0]['username']
if(!tfaEnabled){
var secret = speakeasy.generateSecret({length: 20, name: username+' - solidscribe.com'})
const twoFaSecretToken = secret.base32
const otpauthUrl = secret.otpauth_url
//Generate test Token
var token = speakeasy.totp({
secret: twoFaSecretToken,
encoding: 'base32'
})
db.promise().query('UPDATE user SET two_fa_secret = ? WHERE id = ? LIMIT 1', [twoFaSecretToken, userId])
.then((r,f) => {
QRCode.toDataURL(otpauthUrl, function(err, qrCode) {
//Return A QR code for the user, one time use
return resolve({qrCode, token})
})
})
} else {
return reject('Two factor already enabled for user')
}
})
.catch(error => {
console.log('Key auth error')
console.log(error)
return reject(false)
})
})
}
Auth.setTwoFactorEnabled = (userId, password, token, enable) => {
return new Promise((resolve, reject) => {
Auth.validateTwoFactorToken(userId, password, token)
.then(isValid => {
if(isValid){
db.promise().query('UPDATE user SET two_fa_enabled = ? WHERE id = ? LIMIT 1', [enable, userId])
.then((r, f) => {
return resolve(true)
})
} else {
return resolve(false)
}
})
})
}
Auth.validateTwoFactorToken = (userId, password, token) => {
return new Promise((resolve, reject) => {
const User = require('@models/User')
User.getMasterKey(userId, password)
.then(masterKey => {
return db.promise().query('SELECT two_fa_secret FROM user WHERE id = ?', [userId])
})
.then((r,f) => {
//Verify Token
const tokenValidates = speakeasy.totp.verify({
'secret': r[0][0]['two_fa_secret'],
'encoding': 'base32',
'token': token,
'window': 6
})
return resolve(tokenValidates)
})
.catch(error => {
console.log('Token Validation Error')
return resolve(false)
})
})
}
Auth.testTwoFactor = () => {
const userId = 93
const pass = '1'
let tfaToken = null
console.log('Test Two Factor')
Auth.generateTwoFactorSecretKey(userId, pass)
.then( ({qrCode, token}) => {
tfaToken = token
Auth.validateTwoFactorToken(userId, pass, tfaToken)
.then(validToken => {
console.log('Is Token Valid:', validToken)
})
return Auth.setTwoFactorEnabled(userId, pass, tfaToken, true)
})
.then(twoFactorEnbled => {
console.log('Was it enabled?', twoFactorEnbled)
return Auth.setTwoFactorEnabled(userId, pass, tfaToken, false)
})
.then(twoFactorEnbled => {
console.log('Was it disabled?', twoFactorEnbled)
})
.catch(error => {
console.log(error)
})
}
Auth.test = () => {
const testUserId = 22
+4 -2
View File
@@ -259,13 +259,15 @@ const printResults = true
let UserTest = require('@models/User')
let NoteTest = require('@models/Note')
let AuthTest = require('@helpers/Auth')
Auth.testTwoFactor()
Auth.test()
UserTest.keyPairTest('genMan15', '1', printResults)
UserTest.keyPairTest('genMan16', '1', printResults)
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
.then( message => {
if(printResults) console.log(message)
})
// Test Area
//Test
+5 -5
View File
@@ -182,7 +182,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
const encryptedText = cs.encrypt(masterKey, salt, textObject)
db.promise()
.query(`INSERT INTO note_raw_text (text, salt, updated) VALUE (?, ?, ?)`, [encryptedText, salt, created])
.query(`INSERT INTO note_raw_text (text, salt, updated) VALUE (?, ?, ?)`, [encryptedText, salt, (+new Date)])
.then( (rows, fields) => {
const rawTextId = rows[0].insertId
@@ -515,7 +515,7 @@ Note.setPinned = (userId, noteId, pinnedBoolean) => {
return new Promise((resolve, reject) => {
const pinned = pinnedBoolean ? 1:0
const now = Math.round((+new Date)/1000)
const now = (+new Date)
//Update other note attributes
return db.promise()
@@ -532,7 +532,7 @@ Note.setArchived = (userId, noteId, archivedBoolead) => {
return new Promise((resolve, reject) => {
const archived = archivedBoolead ? 1:0
const now = Math.round((+new Date)/1000)
const now = (+new Date)
//Update other note attributes
return db.promise()
@@ -549,7 +549,7 @@ Note.setTrashed = (userId, noteId, trashedBoolean, masterKey) => {
return new Promise((resolve, reject) => {
const trashed = trashedBoolean ? 1:0
const now = Math.round((+new Date)/1000)
const now = (+new Date)
//Update other note attributes
return db.promise()
@@ -1048,7 +1048,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
// Always prioritize pinned notes in searches.
//Default Sort, order by last updated
let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, note.created DESC, note.opened DESC, id DESC'
let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, note.created DESC, note.opened DESC'
//Order by Last Created Date
if(fastFilters.lastCreated == 1){
+85 -28
View File
@@ -5,22 +5,32 @@ const Note = require('@models/Note')
const db = require('@config/database')
const Auth = require('@helpers/Auth')
const cs = require('@helpers/CryptoString')
const speakeasy = require('speakeasy')
let User = module.exports = {}
const version = '3.0.1'
const version = '3.1.3'
//Login a user, if that user does not exist create them
//Issues login token
User.login = (username, password) => {
User.login = (username, password, authToken = null) => {
return new Promise((resolve, reject) => {
const lowerName = username.toLowerCase();
const lowerName = username.toLowerCase()
let statusObject = {
success: false,
token: null,
userId: null,
verificationRequired: false,
message: 'Incorrect Username or Password'
}
db.promise()
.query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName])
.then((rows, fields) => {
//
// Login User
//
if(rows[0].length == 1){
@@ -28,34 +38,76 @@ User.login = (username, password) => {
//Pull out user data from database results
const lookedUpUser = rows[0][0]
//hash the password and check for a match
// const salt = new Buffer(lookedUpUser.salt, 'binary')
const salt = Buffer.from(lookedUpUser.salt, 'binary')
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
if(delivered_key.toString('hex') === lookedUpUser.password){
//Verify Token if set
const tokenValidates = speakeasy.totp.verify({
'secret': lookedUpUser['two_fa_secret'],
'encoding': 'base32',
'token': authToken,
'window': 2
})
User.generateMasterKey(lookedUpUser.id, password)
.then( result => User.getMasterKey(lookedUpUser.id, password))
.then(masterKey => {
if(lookedUpUser.two_fa_enabled == 1 && !authToken){
User.generateKeypair(lookedUpUser.id, masterKey)
.then(({publicKey, privateKey}) => {
statusObject['verificationRequired'] = true
statusObject['message'] = '2FA authentication required.'
//Passback a json web token
Auth.createToken(lookedUpUser.id, masterKey)
.then(token => {
return resolve({ token: token, userId:lookedUpUser.id })
return resolve(statusObject)
}
if(lookedUpUser.two_fa_enabled == 1 && !tokenValidates){
statusObject['verificationRequired'] = true
statusObject['message'] = 'Invalid Authorization Token.'
return resolve(statusObject)
}
if(lookedUpUser.two_fa_enabled == 0 || (lookedUpUser.two_fa_enabled == 1 && tokenValidates) ){
//hash the password and check for a match
const salt = Buffer.from(lookedUpUser.salt, 'binary')
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
if(delivered_key.toString('hex') === lookedUpUser.password){
User.generateMasterKey(lookedUpUser.id, password)
.then( result => User.getMasterKey(lookedUpUser.id, password))
.then(masterKey => {
User.generateKeypair(lookedUpUser.id, masterKey)
.then(({publicKey, privateKey}) => {
//Passback a json web token
Auth.createToken(lookedUpUser.id, masterKey)
.then(token => {
statusObject['token'] = token
statusObject['userId'] = lookedUpUser.id
statusObject['success'] = true
return resolve(statusObject)
})
})
})
})
} else {
} else {
return resolve(statusObject)
}
})
}
reject('Password does not match database')
}
})
} else {
return reject('Incorrect Username or Password')
//If user is not found, say two factor authentication is required
statusObject['verificationRequired'] = true
statusObject['message'] = '2FA authentication required.'
//Show fake auth token message
if(authToken){
statusObject['message'] = 'Invalid Authorization Token.'
}
return resolve(statusObject)
}
})
.catch(console.log)
@@ -186,6 +238,12 @@ User.getCounts = (userId) => {
Object.assign(countTotals, rows[0][0]) //combine results
return db.promise().query('SELECT two_fa_enabled FROM user WHERE id = ?', [userId])
}).then( (rows, fields) => {
Object.assign(countTotals, rows[0][0]) //combine results
//Convert everything to an int or 0
Object.keys(countTotals).forEach( key => {
const count = parseInt(countTotals[key])
@@ -253,13 +311,12 @@ User.generateMasterKey = (userId, password) => {
}
User.getMasterKey = (userId, password) => {
if(!userId || !password){
reject('Need userId and password to fetch key')
}
return new Promise((resolve, reject) => {
if(!userId || !password){
reject('Need userId and password to fetch key')
}
db.promise().query('SELECT * FROM user_key WHERE user_id = ? LIMIT 1', [userId])
.then((rows, fields) => {
+30 -20
View File
@@ -1,7 +1,8 @@
var express = require('express')
var router = express.Router()
let User = require('@models/User');
const User = require('@models/User')
const Auth = require('@helpers/Auth')
const cs = require('@helpers/CryptoString')
// middleware that is specific to this router
@@ -9,26 +10,14 @@ router.use(function timeLog (req, res, next) {
// console.log('Time: ', Date.now())
next()
})
// define the home page route
router.get('/', function (req, res) {
res.send('User Home Page ' + User.getUsername())
})
// define the about route
router.get('/about', function (req, res) {
User.getUsername(req.headers.userId)
.then( data => res.send(data) )
})
// Login User
router.post('/login', function (req, res) {
User.login(req.body.username, req.body.password)
User.login(req.body.username, req.body.password, req.body.authToken)
.then( returnData => {
res.send(returnData)
})
.catch(e => {
res.send(false)
})
})
// Logout User
router.post('/logout', function (req, res) {
@@ -39,10 +28,7 @@ router.post('/logout', function (req, res) {
})
})
// Login User
// Register User
router.post('/register', function (req, res) {
User.register(req.body.username, req.body.password)
@@ -55,12 +41,36 @@ router.post('/register', function (req, res) {
})
})
// fetch counts of users notes
router.post('/totals', function (req, res) {
User.getCounts(req.headers.userId)
.then( countsObject => res.send( countsObject ))
})
//
// Two Factor Auth Setup
//
router.post('/twofactorsetup', function (req, res) {
//Send QR code to user for 2FA setup
Auth.generateTwoFactorSecretKey(req.headers.userId, req.body.password)
.then( ({ qrCode }) => { res.send( qrCode ) })
})
router.post('/verifytwofactorsetuptoken', function (req, res) {
//Verify Users QR code with token
Auth.setTwoFactorEnabled(req.headers.userId, req.body.password, req.body.token, true)
.then( ( results ) => { res.send( results ) })
})
router.post('/validatetwofactortoken', function (req, res) {
//Verify Users QR code with token
Auth.validateTwoFactorToken(req.headers.userId, req.body.password, req.body.token)
.then( ( results ) => { res.send( results ) })
})
module.exports = router