Compare commits

...

2 Commits

Author SHA1 Message Date
Max G
e7d1cc7bc9 * Added theme colors to form fields
* Added some basic table styles for inserting some shitty tables
* Made popup notification styles look better and work better on mobile
* Quick note now opens a note and not some weird page
* Menu collapses when page is small, behaves like mobile menu
* Added terms and conditions to help and login forms
* Added password change functionality
* Better styles for shared page
* Added some tests for changing password
2020-07-14 05:31:02 +00:00
Max G
47fff0e1ee Added privacy policy
Updated marketing
Added some keyboard shortcuts
Added settings page
Added accent theming
Added beta 2FA
2020-07-07 04:04:55 +00:00
35 changed files with 1766 additions and 419 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -97,6 +97,12 @@ export default {
//Detect if user is on a mobile browser and set a flag in store //Detect if user is on a mobile browser and set a flag in store
this.$store.commit('detectIsUserOnMobile') 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 //Set color theme based on local storage
const themeNumber = localStorage.getItem('nightMode') const themeNumber = localStorage.getItem('nightMode')
if(themeNumber != null){ if(themeNumber != null){

View File

@ -4,6 +4,10 @@ const helpers = {}
helpers.timeAgo = (time) => { helpers.timeAgo = (time) => {
if(time == null){
time = Math.round(time/1000)
}
if(time.toString().length >= 13){ if(time.toString().length >= 13){
time = Math.round(time/1000) time = Math.round(time/1000)
} }

View File

@ -18,7 +18,8 @@
:root { :root {
--main-accent: #16ab39; /*main accent for all buttons, icons and logos*/
--main-accent: #21BA45;
/*theme colors */ /*theme colors */
--body_bg_color: #f5f6f7; --body_bg_color: #f5f6f7;
@ -85,6 +86,8 @@ body {
text-align: center; text-align: center;
} }
.ui.form input:not([type]), .ui.form input:not([type]),
.ui.form input:not([type]):focus, .ui.form input:not([type]):focus,
.ui.form textarea:not([type]), .ui.form textarea:not([type]),
@ -93,6 +96,21 @@ body {
background-color: var(--small_element_bg_color); background-color: var(--small_element_bg_color);
border-color: var(--dark_border_color); border-color: var(--dark_border_color);
} }
.ui.form input[type="password"],
.ui.form input[type="text"],
.ui.input > input {
color: var(--text_color);
background-color: var(--small_element_bg_color);
border-color: var(--dark_border_color);
}
.ui.form input[type="password"]:focus, .ui.form input[type="password"]:active,
.ui.form input[type="text"]:focus, .ui.form input[type="text"]:active,
.ui.input > input:focus, .ui.input > input:active {
color: var(--text_color);
background-color: var(--small_element_bg_color);
border-color: var(--main-accent);
border-right-color: var(--main-accent) !important;
}
.ui.basic.label, .ui.header, .ui.header div.sub.header { .ui.basic.label, .ui.header, .ui.header div.sub.header {
color: var(--text_color); color: var(--text_color);
background-color: transparent; background-color: transparent;
@ -105,7 +123,7 @@ div.ui.basic.green.label {
background-color: var(--small_element_bg_color) !important; background-color: var(--small_element_bg_color) !important;
} }
.ui.basic.button, .ui.basic.buttons .button { .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; color: var(--text_color) !important;
border: 1px solid; border: 1px solid;
border-color: var(--dark_border_color) !important; border-color: var(--dark_border_color) !important;
@ -125,6 +143,24 @@ div.ui.basic.green.label {
color: var(--text_color) !important; color: var(--text_color) !important;
border-color: var(--dark_border_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*/ /* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
/*// /*//
@ -235,7 +271,7 @@ div.ui.basic.green.label {
/*border-bottom: 1px solid #ccc;*/ /*border-bottom: 1px solid #ccc;*/
scrollbar-width: none; scrollbar-width: none;
scrollbar-color: transparent transparent; scrollbar-color: transparent transparent;
caret-color: #21BA45; caret-color: var(--main-accent);
} }
.squire-box::selection, .squire-box::selection,
.squire-box::-moz-selection { .squire-box::-moz-selection {
@ -253,7 +289,7 @@ div.ui.basic.green.label {
cursor: pointer; cursor: pointer;
} }
.night-mode .note-card-text i:not(.icon), .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); background-color: rgba(255, 255, 255, 0.2);
} }
@ -322,9 +358,76 @@ div.ui.basic.green.label {
font-family: 'Icons'; font-family: 'Icons';
content: "\f058"; content: "\f058";
color: #21BA45; color: var(--main-accent);
opacity: 1; 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;
}
tr {
display: flex;
}
th, td {
border: 1px solid #ddd;
border-bottom: 1px solid #ddd;
font-weight: normal;
flex: 1;
}
/* table:hover th, table:hover td {
border: 1px solid black;
}*/
th, td {
padding: 3px;
text-align: left;
}
.table-tic-table {
}
.table-tic-table > div {
height: 21px;
margin: 0;
padding: 0;
}
.tabletic {
display: inline-block;
border: 1px solid black;
border-radius: 2px;
width: 20px;
height: 20px;
margin: 0 1px 1px 0;
cursor: pointer;
}
.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 */ /* adjust checkboxes for mobile. Make them a little bigger, easier to click */
@media only screen and (max-width: 740px) { @media only screen and (max-width: 740px) {
@ -365,7 +468,7 @@ div.ui.basic.green.label {
font-family: 'Icons'; font-family: 'Icons';
content: "\f058"; content: "\f058";
color: #21BA45; color: var(--main-accent);
opacity: 1; opacity: 1;
} }
} }

View File

@ -1096,6 +1096,9 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
} }
while ( true ) { while ( true ) {
if ( endContainer === endMax || endContainer === root ) {
break;
}
if ( maySkipBR && if ( maySkipBR &&
endContainer.nodeType !== TEXT_NODE && endContainer.nodeType !== TEXT_NODE &&
endContainer.childNodes[ endOffset ] && endContainer.childNodes[ endOffset ] &&
@ -1103,9 +1106,7 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
endOffset += 1; endOffset += 1;
maySkipBR = false; maySkipBR = false;
} }
if ( endContainer === endMax || if ( endOffset !== getLength( endContainer ) ) {
endContainer === root ||
endOffset !== getLength( endContainer ) ) {
break; break;
} }
parent = endContainer.parentNode; parent = endContainer.parentNode;
@ -1117,6 +1118,20 @@ var moveRangeBoundariesUpTree = function ( range, startMax, endMax, root ) {
range.setEnd( endContainer, endOffset ); 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, // Returns the first block at least partially contained by the range,
// or null if no block is contained by the range. // or null if no block is contained by the range.
var getStartBlockOfRange = function ( range, root ) { var getStartBlockOfRange = function ( range, root ) {
@ -1285,10 +1300,13 @@ var onKey = function ( event ) {
if ( event.altKey ) { modifiers += 'alt-'; } if ( event.altKey ) { modifiers += 'alt-'; }
if ( event.ctrlKey ) { modifiers += 'ctrl-'; } if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
if ( event.metaKey ) { modifiers += 'meta-'; } if ( event.metaKey ) { modifiers += 'meta-'; }
if ( event.shiftKey ) { modifiers += 'shift-'; }
} }
// However, on Windows, shift-delete is apparently "cut" (WTF right?), so // However, on Windows, shift-delete is apparently "cut" (WTF right?), so
// we want to let the browser handle shift-delete. // we want to let the browser handle shift-delete in this situation.
if ( event.shiftKey ) { modifiers += 'shift-'; } if ( isWin && event.shiftKey && key === 'delete' ) {
modifiers += 'shift-';
}
key = modifiers + key; key = modifiers + key;
@ -1465,12 +1483,7 @@ var handleEnter = function ( self, shiftKey, range ) {
// just play it safe and insert a <br>. // just play it safe and insert a <br>.
if ( !block || shiftKey || /^T[HD]$/.test( block.nodeName ) ) { if ( !block || shiftKey || /^T[HD]$/.test( block.nodeName ) ) {
// If inside an <a>, move focus out // If inside an <a>, move focus out
parent = getNearest( range.endContainer, root, 'A' ); moveRangeBoundaryOutOf( range, 'A', root );
if ( parent ) {
parent = parent.parentNode;
moveRangeBoundariesUpTree( range, parent, parent, root );
range.collapse( false );
}
insertNodeInRange( range, self.createElement( 'BR' ) ); insertNodeInRange( range, self.createElement( 'BR' ) );
range.collapse( false ); range.collapse( false );
self.setSelection( range ); 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 + 'b' ] = mapKeyToFormat( 'B' );
keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' ); keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' );
keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' ); keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' );
keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' ); keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' );
keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } ); keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } );
keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } ); keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } );
keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' ); keyHandlers[ ctrlKey + 'shift-8' ] =
keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' ); toggleList( /(?:^|>)UL/, 'makeUnorderedList' );
keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' ); keyHandlers[ ctrlKey + 'shift-9' ] =
keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' ); toggleList( /(?:^|>)OL/, 'makeOrderedList' );
keyHandlers[ ctrlKey + '[' ] =
changeIndentationLevel( 'decreaseQuoteLevel', 'decreaseListLevel' );
keyHandlers[ ctrlKey + ']' ] =
changeIndentationLevel( 'increaseQuoteLevel', 'increaseListLevel' );
keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' ); keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' );
keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' ); keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' ); keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
@ -4417,6 +4459,12 @@ proto.insertHTML = function ( html, isPaste ) {
this._docWasChanged(); this._docWasChanged();
} }
range.collapse( false ); 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(); this._ensureBottomLine();
} }
@ -4947,4 +4995,4 @@ if ( typeof exports === 'object' ) {
} }
} }
}( document ) ); }( document ) );

View File

@ -182,7 +182,6 @@
openNote(){ openNote(){
const noteId = this.item.note_id const noteId = this.item.note_id
this.$router.push('/notes/open/'+noteId) this.$router.push('/notes/open/'+noteId)
this.$bus.$emit('open_note', noteId)
}, },
openEditAttachments(){ openEditAttachments(){
const noteId = this.item.note_id const noteId = this.item.note_id

View File

@ -24,9 +24,9 @@
} }
}, },
beforeMount(){ beforeMount(){
this.$bus.$on('reset_fast_filters', () => { // this.$bus.$on('reset_fast_filters', () => {
this.orderString = 'Order by Last Edited' // this.orderString = 'Order by Last Edited'
}) // })
}, },
methods:{ methods:{
displayString(){ displayString(){

View File

@ -2,30 +2,31 @@
.popup-body { .popup-body {
position: fixed; position: fixed;
bottom: 15px; top: 15px;
left: 15px; left: 15px;
min-height: 50px; min-height: 50px;
min-width: 200px; min-width: 200px;
max-width: calc(100% - 20px); max-width: calc(100% - 30px);
z-index: 1002; z-index: 1002;
border-top: 2px solid #21ba45;
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1); box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
border-top-right-radius: 4px; border-radius: 4px;
border-top-left-radius: 4px;
color: white;
background-color: var(--main-accent);
} }
.popup-row { .popup-row {
padding: 1em 5px; padding: 1em 5px;
cursor: pointer; cursor: pointer;
white-space: nowrap;
} }
.popup-row > span { .popup-row > span {
width: calc(100% - 50px); /*width: calc(100% - 50px);*/
display: inline-block; display: inline-block;
text-align: center; text-align: left;
box-sizing: border-box; box-sizing: border-box;
padding: 0 10px 0; padding: 0 10px 0;
font-size: 1.25em; font-size: 1.25em;
border-radius: 4px;
} }
.popup-row + .popup-row { .popup-row + .popup-row {
border-top: 1px solid #FFF; border-top: 1px solid #FFF;
@ -36,12 +37,10 @@
} }
@keyframes slide-in-bottom { @keyframes slide-in-bottom {
0% { 0% {
transform: translateY(1000px); transform: translateY(-1000px);
opacity: 0;
} }
100% { 100% {
transform: translateY(0); transform: translateY(0);
opacity: 1;
} }
} }
@ -63,14 +62,46 @@
} }
} }
.meter {
height: 2px;
display: inline-block;
width: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
overflow: hidden;
border-top-right-radius: 4px;
border-top-left-radius: 4px;
}
.meter span {
display: block;
height: 100%;
}
.progress {
background-color: white;
animation: progressBar 3s linear;
animation-fill-mode: both;
}
@keyframes progressBar {
0% { width: 0; }
100% { width: 100%; }
}
</style> </style>
<template> <template>
<div class="popup-body slide-in-bottom" v-on:click="dismiss" v-if="notifications.length > 0"> <div class="popup-body slide-in-bottom" v-on:click="dismiss" v-if="notifications.length > 0">
<div class="popup-row color-fade" v-for="item in notifications"> <div class="popup-row" v-for="item in notifications">
<span>{{ item }}</span> <div class="meter">
<span><span class="progress"></span></span>
</div>
<span><i class="small info circle icon"></i>{{ item }}</span>
</div> </div>
</div> </div>
</template> </template>
@ -94,9 +125,9 @@
}, },
mounted(){ mounted(){
// this.$bus.$emit('notification', 'Password Protection Removed') // this.$bus.$emit('notification', 'Password Protection Removed Login did not succeed')
// this.$bus.$emit('notification', 'Password Protection Removed') // this.$bus.$emit('notification', 'Password Protection Removed your life is exposed to the internet')
// this.$bus.$emit('notification', 'Password Protection Removed') // this.$bus.$emit('notification', 'Password Protection Removed everyone can see everything')
}, },
methods: { methods: {
@ -105,7 +136,7 @@
clearTimeout(this.totalTimeout) clearTimeout(this.totalTimeout)
this.totalTimeout = setTimeout(() => { this.totalTimeout = setTimeout(() => {
this.dismiss() this.dismiss()
}, 4000) }, 3000)
}, },
dismiss(){ dismiss(){
this.notifications = [] this.notifications = []

View File

@ -19,9 +19,10 @@
bottom: 0; bottom: 0;
} }
.menu-logo-display { .menu-logo-display {
width: 25px; width: 27px;
margin: 5px 0 0 42px; margin: 5px 0 0 41px;
display: inline-block; display: inline-block;
height: auto;
} }
.menu-item { .menu-item {
@ -79,15 +80,17 @@
background-color: var(--small_element_bg_color); background-color: var(--small_element_bg_color);
border-bottom: 1px solid; border-bottom: 1px solid;
border-color: var(--border_color); border-color: var(--border_color);
padding: 5px 1rem 5px; /*padding: 5px 1rem 5px;*/
display: flex;
justify-content: space-around;
} }
.place-holder { .place-holder {
width: 100%; width: 100%;
height: 50px; height: 50px;
} }
.top-menu-bar img { .logo-display {
width: 30px; width: 27px;
height: 30px; height: auto;
} }
.version-display { .version-display {
position: absolute; position: absolute;
@ -101,6 +104,19 @@
cursor: pointer; 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> </style>
<template> <template>
@ -110,45 +126,51 @@
<!-- collapsed menu, appears as a bar --> <!-- collapsed menu, appears as a bar -->
<div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen"> <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>
<div class="mobile-button"></div>
<!-- open straight to note -->
<router-link
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
exact-active-class="active"
class="mobile-button"
:to="`/notes/open/${$store.getters.totals['quickNote']}`">
<i class="green sticky note outline icon"></i>
</router-link>
<!-- create new and redirect to new note id -->
<a
v-if="loggedIn && $store.getters.totals && !$store.getters.totals['quickNote']"
v-on:click="newQuickNote()"
exact-active-class="active"
class="mobile-button">
<i class="green sticky note outline icon"></i>
</a>
<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>
<div class="shade" v-if="mobile && !collapsed" v-on:click="collapseMenu"></div> <div class="shade" v-if="mobile && !collapsed" v-on:click="collapseMenu"></div>
@ -159,10 +181,8 @@
<div class="global-menu" v-if="!collapsed" v-on:click="menuClicked"> <div class="global-menu" v-if="!collapsed" v-on:click="menuClicked">
<div class="menu-section" v-on:click="collapseMenu"> <div class="menu-section" v-on:click="collapseMenu">
<!-- <div class="menu-item menu-button" > --> <i class="white angle left icon"></i>
<i class="white angle left icon"></i> <logo class="menu-logo-display" color="var(--main-accent)" />
<img class="menu-logo-display" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
<!-- </div> -->
</div> </div>
<div class="menu-section" v-if="loggedIn"> <div class="menu-section" v-if="loggedIn">
@ -208,9 +228,26 @@
</div> </div>
<div class="menu-section" v-if="loggedIn"> <div class="menu-section" v-if="loggedIn">
<router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick">
<!-- open straight to note -->
<router-link
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
exact-active-class="active"
class="menu-item menu-button"
:to="`/notes/open/${$store.getters.totals['quickNote']}`">
<i class="sticky note outline icon"></i>Scratch Pad <i class="sticky note outline icon"></i>Scratch Pad
</router-link> </router-link>
<!-- create new and redirect to new note id -->
<a
v-if="loggedIn && $store.getters.totals && !$store.getters.totals['quickNote']"
v-on:click="newQuickNote()"
exact-active-class="active"
class="menu-item menu-button">
<i class="sticky note outline icon"></i>Scratch Pad
</a>
</div> </div>
<div class="menu-section" v-if="!loggedIn"> <div class="menu-section" v-if="!loggedIn">
@ -236,14 +273,14 @@
<div class="menu-section"> <div class="menu-section">
<router-link class="menu-item menu-button" exact-active-class="active" to="/help"> <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> </router-link>
</div> </div>
<div class="menu-section" v-if="loggedIn" :data-tooltip="`Logout ${this.$store.getters.getUsername}`" data-inverted="" data-position="right center"> <div class="menu-section" v-if="loggedIn" :data-tooltip="`Settings for ${this.$store.getters.getUsername}`" data-inverted="" data-position="right center">
<div v-on:click="destroyLoginToken" class="menu-item menu-button"> <router-link class="menu-item menu-button" exact-active-class="active" to="/settings">
<i v-if="userIcon" class="user outline icon"></i>{{ usernameDisplay }} <i v-if="userIcon" class="cog icon"></i>{{ usernameDisplay }}
</div> </router-link>
</div> </div>
@ -264,6 +301,7 @@
components: { components: {
'search-input': require('@/components/SearchInput.vue').default, 'search-input': require('@/components/SearchInput.vue').default,
'counter':require('@/components/AnimatedCounterComponent.vue').default, 'counter':require('@/components/AnimatedCounterComponent.vue').default,
'logo':require('@/components/LogoComponent.vue').default,
}, },
data: function(){ data: function(){
return { return {
@ -274,10 +312,14 @@
disableNewNote: false, disableNewNote: false,
menuOpen: true, menuOpen: true,
userIcon: true, userIcon: true,
resizeDebounce: null,
} }
}, },
beforeCreate: function(){ beforeMount(){
window.addEventListener('resize', this.resizeEventHandler)
},
beforeDestroy(){
window.removeEventListener('resize', this.resizeEventHandler)
}, },
mounted: function(){ mounted: function(){
this.mobile = this.$store.getters.getIsUserOnMobile this.mobile = this.$store.getters.getIsUserOnMobile
@ -291,6 +333,8 @@
this.$store.dispatch('fetchAndUpdateUserTotals') this.$store.dispatch('fetchAndUpdateUserTotals')
this.version = localStorage.getItem('currentVersion') this.version = localStorage.getItem('currentVersion')
} }
this.resizeEventHandler() //Trigger resize event
}, },
computed: { computed: {
@ -316,6 +360,32 @@
}, },
}, },
methods: { methods: {
newQuickNote(){
axios.post('/api/quick-note/get')
.then( ({data}) => {
console.log(data)
this.$router.push({'path':'/notes/open/'+data.noteId})
})
},
resizeEventHandler(e) {
clearTimeout(this.resizeDebounce)
this.resizeDebounce = setTimeout(() => {
this.mobile = false
this.menuOpen = false
this.collapsed = false
if(window.innerWidth < 700){
this.collapsed = true
this.mobile = true
}
}, 100)
},
menuClicked(){ menuClicked(){
//Collapse menu when item is clicked in mobile //Collapse menu when item is clicked in mobile
if(this.mobile && !this.collapsed){ if(this.mobile && !this.collapsed){
@ -334,28 +404,22 @@
}, },
createNote(event){ createNote(event){
const title = ''
this.disableNewNote = true this.disableNewNote = true
axios.post('/api/note/create', {title}) axios.post('/api/note/create', {title:''})
.then(response => { .then(response => {
if(response.data && response.data.id){ 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 this.disableNewNote = false
} }
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') }) .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(){ toggleNightMode(){
this.$store.commit('toggleNightMode') this.$store.commit('toggleNightMode')
}, },

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="loading-container"> <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"> <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 <animateTransform
attributeName="transform" attributeName="transform"
dur="0.5s" dur="0.5s"
@ -12,7 +12,7 @@
attributeType="XML" attributeType="XML"
begin="rectBox.end"/> begin="rectBox.end"/>
</rect> </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 <animate
attributeName="height" attributeName="height"
dur="1.3s" dur="1.3s"

View File

@ -1,3 +1,4 @@
<template> <template>
<div v-on:keyup.enter="login()"> <div v-on:keyup.enter="login()">
@ -14,6 +15,11 @@
<input v-model="password" type="password" name="password" placeholder="Password"> <input v-model="password" type="password" name="password" placeholder="Password">
</div> </div>
</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="sixteen wide field">
<div class="ui fluid buttons"> <div class="ui fluid buttons">
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui green button"> <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>
</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> </div>
<!-- Thin form display --> <!-- Thin form display -->
<div v-if="thin" class="ui small form"> <div v-if="thin" class="ui small form">
<div class="fields"> <div class="equal width fields">
<div class="four wide field"> <div class="field">
<div class="ui input"> <div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail"> <input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
</div> </div>
</div> </div>
<div class="four wide field"> <div class="field">
<div class="ui input"> <div class="ui input">
<input v-model="password" type="password" name="password" placeholder="Password"> <input v-model="password" type="password" name="password" placeholder="Password">
</div> </div>
</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"> <div v-on:click="register()" class="ui fluid green button">
<i class="plug icon"></i> <i class="plug icon"></i>
Sign Up Sign Up
</div> </div>
</div> </div>
<div class="four wide field"> <div class="field">
<div v-on:click="login()" class="ui fluid button"> <div v-on:click="login()" class="ui fluid button">
<i class="power icon"></i> <i class="power icon"></i>
Login Login
</div> </div>
</div> </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> </div>
@ -83,7 +109,9 @@
return { return {
enabled: false, enabled: false,
username: '', username: '',
password: '' password: '',
authToken: '',
require2FA: false,
} }
}, },
methods: { methods: {
@ -139,14 +167,30 @@
return 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}) => { .then(({data}) => {
if(data == false){ //Enable 2FA on form
this.$bus.$emit('notification', 'Unable to Login - Incorrect Username or Password') 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 => { .catch(error => {
this.$bus.$emit('notification', 'Unable to Login - Incorrect Username or Password') 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>

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>

View File

@ -2,9 +2,8 @@
<!-- change class to .master-note-edit to have it popup on the screen --> <!-- change class to .master-note-edit to have it popup on the screen -->
<div <div
id="InputNotes" id="InputNotes"
class="master-note-edit full-focus" class="master-note-edit full-focus position-0"
@keyup.esc="close()" @keyup.esc="closeButtonAction()"
:class="[ 'position-'+position ]"
> >
<!-- Giant Edit Note Menu --> <!-- Giant Edit Note Menu -->
@ -14,7 +13,7 @@
<div class="edit-spacer"></div> <div class="edit-spacer"></div>
<div class="menu-top-half"> <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> <i class="close icon"></i>
</div> </div>
@ -61,7 +60,15 @@
</div> </div>
<div class="menu-bottom-half"> <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="removeFormatting()" data-tooltip="Remove Formatting" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="removeFormatting()" data-tooltip="Remove Formatting" data-position="bottom center" data-inverted>
<i class="remove format icon"></i> <i class="remove format icon"></i>
</div> </div>
@ -95,7 +102,8 @@
<div class="edit-divide"></div> <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="green archive icon"></i></span>
<span v-if="archived != 1"><i class="archive icon"></i></span> <span v-if="archived != 1"><i class="archive icon"></i></span>
</div> </div>
@ -105,17 +113,13 @@
<span v-if="pinned != 1"><i class="pin icon"></i></span> <span v-if="pinned != 1"><i class="pin icon"></i></span>
</div> --> </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"> <div class="edit-button" v-if="usersOnNote > 1">
<i class="green eye icon"></i> {{ usersOnNote }} <i class="green eye icon"></i> {{ usersOnNote }}
</div> </div>
<!-- <div class="edit-button" v-on:click="simulateTyping()"> <!--
<div class="edit-button" v-on:click="simulateTyping()">
<i class="purple bolt icon"></i> <i class="purple bolt icon"></i>
</div> --> </div> -->
@ -132,7 +136,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bottom-edit-menu"></div> <div class="bottom-edit-menu"></div>
@ -163,7 +166,7 @@
</textarea> </textarea>
<!-- Squire Box --> <!-- 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> </div>
@ -224,19 +227,19 @@
<div class="sixteen wide column"> <div class="sixteen wide column">
<div class="ui labeled icon fluid basic button" v-on:click="sortList"> <div class="ui labeled icon fluid basic button" v-on:click="sortList">
<i class="sort amount up icon"></i> <i class="sort amount up icon"></i>
Sort List items (Move checked to bottom) Sort List
</div> </div>
</div> </div>
<div class="eight wide column"> <div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="deleteCompletedListItems"> <div class="ui labeled icon fluid basic button" v-on:click="deleteCompletedListItems">
<i class="trash icon"></i> <i class="trash icon"></i>
Delete Checked Items Delete Checked
</div> </div>
</div> </div>
<div class="eight wide column"> <div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="uncheckAllListItems"> <div class="ui labeled icon fluid basic button" v-on:click="uncheckAllListItems">
<i class="list ul icon"></i> <i class="list ul icon"></i>
Uncheck all Checked items Uncheck All
</div> </div>
</div> </div>
<div class="eight wide column"> <div class="eight wide column">
@ -245,6 +248,15 @@
Simple Math Simple Math
</div> </div>
</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"> <div class="sixteen wide column" v-if="rawTextId > 0">
<h2>Share Note</h2> <h2>Share Note</h2>
<share-note-component <share-note-component
@ -257,13 +269,27 @@
</div> </div>
</side-slide-menu> </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">
<h2>Insert Table</h2>
<div class="table-tic-table">
<div v-for="i in 10">
<div v-for="j in 10" class="tabletic" v-on:click="insertTable(i,j)">
</div>
</div>
</div>
</div>
</side-slide-menu>
<!-- Show side shades if user is on desktop only --> <!-- Show side shades if user is on desktop only -->
<div class="full-focus-shade shade1" <div class="full-focus-shade shade1"
:class="{ 'slide-out-left':sizeDown }" :class="{ 'slide-out-left':sizeDown }"
v-on:click="close()"></div> v-on:click="closeButtonAction()"></div>
<div class="full-focus-shade shade2" <div class="full-focus-shade shade2"
:class="{ 'slide-out-right':sizeDown }" :class="{ 'slide-out-right':sizeDown }"
v-on:click="close()"></div> v-on:click="closeButtonAction()"></div>
</div> </div>
</template> </template>
@ -335,6 +361,7 @@
images: false, images: false,
options: false, options: false,
colorpicker: false, colorpicker: false,
table: false,
//Diff text/sync text variables //Diff text/sync text variables
diffTextTimeout: null, diffTextTimeout: null,
@ -355,7 +382,7 @@
//Handle changes in URL to //Handle changes in URL to
if(newVal.id == undefined || newVal.id != this.noteid){ if(newVal.id == undefined || newVal.id != this.noteid){
this.close() // this.closeButtonAction()
} }
//Reset all note menus on URL change //Reset all note menus on URL change
@ -364,6 +391,7 @@
this.tags = false this.tags = false
this.options = false this.options = false
this.images = false this.images = false
this.table = false
//If a menu value is set, open it //If a menu value is set, open it
if(newVal.openMenu){ if(newVal.openMenu){
@ -394,18 +422,20 @@
document.removeEventListener('visibilitychange', this.checkForUpdatedNote) document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
// if(this.editor){ //Obliterate squire instance
this.editor.destroy() this.editor.destroy()
// }
this.close()
}, },
mounted: function() { mounted: function() {
//Show loading for a minimum time
setTimeout(()=>{ setTimeout(()=>{
this.forceShowLoading = false this.forceShowLoading = false
}, 500) }, 500)
document.addEventListener('visibilitychange', this.checkForUpdatedNote) // document.addEventListener('visibilitychange', this.checkForUpdatedNote)
//Init squire as early as possible //Init squire as early as possible
this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' }) this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' })
@ -599,6 +629,30 @@
this.editor.addEventListener('keydown', event => { 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 //Prevent new list items from having
this.$nextTick( () => { this.$nextTick( () => {
//Wait a moment to get item under cursor //Wait a moment to get item under cursor
@ -612,10 +666,6 @@
}) })
}) })
this.editor.addEventListener('keydown', event => {
})
//Bind event handlers //Bind event handlers
this.editor.addEventListener('keyup', event => { this.editor.addEventListener('keyup', event => {
@ -682,7 +732,7 @@
//Block notes you don't have access to from opening //Block notes you don't have access to from opening
if(response.data === false){ if(response.data === false){
this.$bus.$emit('notification', 'Error opening Note') this.$bus.$emit('notification', 'Error opening Note')
this.close(true) this.close()
return return
} }
@ -915,18 +965,21 @@
return hash; 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 // force = true
// console.log(`Close Note ${this.noteid} -> force: ${force}, modified: ${this.modified}`) // console.log(`Close Note ${this.noteid} -> force: ${force}, modified: ${this.modified}`)
if(force){ //Skip everything if foce close is true. Note will just die.
this.$bus.$emit('close_active_note', { if(this.currentNoteId == 0){ return }
noteId: this.noteid, modified: this.modified
})
return
}
this.loadingMessage = 'Saving...' this.loadingMessage = 'Saving...'
this.loading = true this.loading = true
@ -938,14 +991,10 @@
axios.post('/api/note/reindex') axios.post('/api/note/reindex')
} }
this.sizeDown = true this.$bus.$emit('close_active_note', {
//This timeout allows animation to play before closing noteId: this.noteid, modified: this.modified
setTimeout(() => { })
this.$bus.$emit('close_active_note', { return
noteId: this.noteid, modified: this.modified
})
return
}, 300)
}) })
}, },
destroyWebSockets(){ destroyWebSockets(){
@ -1005,8 +1054,8 @@
let element = this.$refs.titleTextarea let element = this.$refs.titleTextarea
let padding = 0 let padding = 0
element.style.height = 'auto'; element.style.height = 'auto'
element.style.height = (element.scrollHeight + padding) +'px'; element.style.height = (element.scrollHeight + padding) +'px'
}, },
} }
} }
@ -1152,7 +1201,7 @@
background-color: var(--menu-accent); background-color: var(--menu-accent);
} }
.edit-active { .edit-active {
background-color: #21BA45; background-color: var(--main-accent);
color: white; color: white;
} }
.edit-divide { .edit-divide {

View File

@ -67,7 +67,7 @@
</span> </span>
<span class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }"> <span class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
{{$helpers.timeAgo(note.updated)}} {{$helpers.timeAgo( note.updated )}}
</span> </span>
<span class="teeny-buttons" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }"> <span class="teeny-buttons" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
@ -234,12 +234,12 @@
}, },
justClosed(){ justClosed(){
//Scroll note into view // Scroll note into view
// this.$el.scrollIntoView({ this.$el.scrollIntoView({
// behavior: 'smooth', behavior: 'smooth',
// block: 'center', block: 'center',
// inline: 'center' inline: 'center'
// }) })
//After scroll, trigger green outline animation //After scroll, trigger green outline animation
setTimeout(() => { setTimeout(() => {
@ -356,13 +356,14 @@
/*Strict font sizes for card display*/ /*Strict font sizes for card display*/
.small-text { .small-text {
max-height: 261px; max-height: 267px;
width: 100%;
overflow: hidden; overflow: hidden;
display: inline-block; display: inline-block;
} }
.small-text, .small-text > p, .small-text > h1, .small-text > h2 { .small-text, .small-text > p, .small-text > h1, .small-text > h2 {
/*font-size: 1.0em !important;*/ /*font-size: 1.0em !important;*/
font-size: 16px !important; font-size: 14px !important;
} }
.small-text > p, , .small-text > h1, .small-text > h2 { .small-text > p, , .small-text > h1, .small-text > h2 {
margin-bottom: 0.5em; margin-bottom: 0.5em;
@ -370,7 +371,7 @@
.big-text > p:first-child, .big-text > p:first-child,
.big-text > h1, .big-text > h2 { .big-text > h1, .big-text > h2 {
/*font-size: 1.3em !important;*/ /*font-size: 1.3em !important;*/
font-size: 17px !important; font-size: 20px !important;
font-weight: bold; font-weight: bold;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@ -415,14 +416,14 @@
border-color: var(--border_color); border-color: var(--border_color);
/*width: calc(33.333% - 10px);*/ /*width: calc(33.333% - 10px);*/
width: calc(25% - 10px); width: calc(25% - 10px);
min-width: 190px; /*min-width: 190px;*/
min-height: 130px; min-height: 130px;
/*transition: box-shadow 0.3s;*/ /*transition: box-shadow 0.3s;*/
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
line-height: 1.8rem; line-height: 1.8rem;
letter-spacing: 0.02rem; letter-spacing: 0.05rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-align: left; text-align: left;
@ -555,27 +556,50 @@
float: right; float: right;
} }
/* Tweak mobile display to show only one column */ /* Break points determine when display cards shrink */
@media only screen and (min-width: 1500px) { @media only screen and (max-width: 700px) {
.note-title-display-card {
width: calc(20% - 10px);
}
}
@media only screen and (max-width: 740px) {
.note-title-display-card { .note-title-display-card {
width: calc(100% + 10px); width: calc(100% + 10px);
margin: 0px -5px 10px -5px; 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*/ /*Animations for cool border effects*/
@keyframes bgin { @keyframes bgin {
0% { 0% {
background-image: background-image:
linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* TopLeft to Right */ linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */
linear-gradient(to bottom, #21BA45 50%, #21BA45 100%), /* TopRight to Bottom */ linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */
linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* BottomLeft to Right*/ linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/
linear-gradient(to bottom, #21BA45 50%, #21BA45 100%); /* TopLeft to Bottom */ linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */
/*Initial state, no BG*/ /*Initial state, no BG*/
background-size: 0 4px, 4px 0, 0 4px, 4px 0; background-size: 0 4px, 4px 0, 0 4px, 4px 0;
} }
@ -590,10 +614,10 @@
30% { 30% {
background-size: 100% 4px, 4px 100%, 100% 4px, 4px 100%; background-size: 100% 4px, 4px 100%, 100% 4px, 4px 100%;
background-image: background-image:
linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* TopLeft to Right */ linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */
linear-gradient(to bottom, #21BA45 50%, #21BA45 100%), /* TopRight to Bottom */ linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */
linear-gradient(to right, #21BA45 50%, #21BA45 100%), /* BottomLeft to Right*/ linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/
linear-gradient(to bottom, #21BA45 50%, #21BA45 100%); /* TopLeft to Bottom */ linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */
} }
100% { 100% {
background-image: background-image:

View File

@ -98,13 +98,17 @@
}, },
beforeCreate: function(){ beforeCreate: function(){
}, },
mounted: function(){ beforeMount(){
//search clear //search clear
this.$bus.$on('reset_fast_filters', () => { this.$bus.$on('reset_fast_filters', () => {
this.searchTerm = '' this.searchTerm = ''
this.tagSuggestions = [] this.tagSuggestions = []
}) })
},
beforeDestroy(){
this.$bus.$off('reset_fast_filters')
},
mounted: function(){
}, },
methods: { methods: {
@ -148,7 +152,6 @@
if(response.data && response.data.id){ if(response.data && response.data.id){
this.$router.push('/notes/open/'+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') }) .catch(error => { this.$bus.$emit('notification', 'Failed to create note') })

View File

@ -12,6 +12,7 @@
<ul> <ul>
<li>Shared notes can be read and edited by you and all shared users.</li> <li>Shared notes can be read and edited by you and all shared users.</li>
<li>Shared notes can only be shared by the creator of the note.</li> <li>Shared notes can only be shared by the creator of the note.</li>
<li>If you turn off sharing, no one else can read the note.</li>
</ul> </ul>
</div> </div>

View File

@ -17,6 +17,7 @@ const SquireButtonFunctions = {
// //
pathChangeEvent(e){ pathChangeEvent(e){
//Reset all button states //Reset all button states
this.activeBold = false this.activeBold = false
this.activeTitle = false this.activeTitle = false
@ -143,28 +144,23 @@ const SquireButtonFunctions = {
// Uncheck All List Items // 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 //Fetch the container
let container = document.getElementById('squire-id') let container = document.getElementById('squire-id')
Array.from( container.getElementsByClassName('active') ).forEach(item => { Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active'); item.classList.remove('active');
}) })
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
}, },
deleteCompletedListItems(){ deleteCompletedListItems(){
// //
// Delete Completed List Items // 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 //Fetch the container
let container = document.getElementById('squire-id') 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(){ sortList(){
// //
// Sort list, checked at the bottom, unchecked at the top // 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 //Fetch the container
let container = document.getElementById('squire-id') 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(){ calculateMath(){
// //
// Find math in note and calculate the outcome // 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 //Fetch the container
let container = document.getElementById('squire-id') 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){ setText(inText){
@ -338,6 +339,33 @@ const SquireButtonFunctions = {
return this.editor.getHTML() return this.editor.getHTML()
}, },
insertDivide(){
this.editor.insertHTML(`<p><div class='divide'></div><br></p>`)
this.editor.focus()
this.editor.moveCursorToEnd()
},
insertTable(tall, wide){
console.log(`Table: ${wide} x ${tall}`)
//Insert a table
let tableSyntax = '<div>'
tableSyntax += '<table>'
for (let i = 0; i < tall; i++) {
tableSyntax += '<tr>'
for (let j = 0; j < wide; j++) {
tableSyntax += '<td><p><br></p></td>'
}
tableSyntax += '</tr>'
}
tableSyntax += '</table></div><p><br></p>'
this.editor.insertHTML(tableSyntax)
this.editor.focus()
this.editor.moveCursorToEnd()
this.$router.go(-1)
},
}, },
} }

File diff suppressed because one or more lines are too long

View File

@ -110,7 +110,7 @@
</h2> </h2>
<h3 class="subtext"> <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> </h3>
</div> </div>
@ -210,18 +210,7 @@
<i class="bottom left corner blue pen icon"></i> <i class="bottom left corner blue pen icon"></i>
</i> </i>
Document Editing Tools Document Editing Tools
<div class="sub header">Bold, Underline, Title, Add Links, Add Tables Color Text, Color Background and more.</div> <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> </div>
</h2> </h2>
@ -236,6 +225,17 @@
</div> </div>
</h2> </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"> <h2 class="ui dividing header">
<div class="content"> <div class="content">
<i class="icons"> <i class="icons">
@ -254,7 +254,7 @@
<i class="bottom left corner share icon"></i> <i class="bottom left corner share icon"></i>
</i> </i>
Share Encrypted Notes 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> </div>
</h2> </h2>
@ -290,6 +290,17 @@
<div class="sub header">Add text to Images and links than can be searched.</div> <div class="sub header">Add text to Images and links than can be searched.</div>
</div> </div>
</h2> </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>
<div class="four wide column"> <div class="four wide column">

View File

@ -25,7 +25,7 @@
</div> </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> </div>
</div> </div>

View File

@ -31,7 +31,7 @@
<i v-if="titleView" class="th icon"></i> <i v-if="titleView" class="th icon"></i>
<i v-if="!titleView" class="bars icon"></i> <i v-if="!titleView" class="bars icon"></i>
</div> </div>
</div> </div>
<div class="eight wide column" v-if="showClear"> <div class="eight wide column" v-if="showClear">
@ -117,7 +117,7 @@
:onClick="openNote" :onClick="openNote"
:data="note" :data="note"
:title-view="titleView" :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" :key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
/> />
</div> </div>
@ -132,13 +132,12 @@
</div> </div>
<input-notes <note-input-panel
v-if="activeNoteId1 != null" v-if="activeNoteId1 != null"
:key="'active_note_'+activeNoteId1" :key="activeNoteId1"
:noteid="activeNoteId1" :noteid="activeNoteId1"
:position="activeNote1Position"
:url-data="$route.params" :url-data="$route.params"
ref="note1" /> />
</div> </div>
</template> </template>
@ -151,7 +150,7 @@
name: 'SearchBar', name: 'SearchBar',
components: { 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, 'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
// 'fast-filters': require('@/components/FastFilters.vue').default, // 'fast-filters': require('@/components/FastFilters.vue').default,
@ -247,7 +246,7 @@
//Do not update note if its open //Do not update note if its open
if(this.activeNoteId1 != noteId){ if(this.activeNoteId1 != noteId){
this.updateSingleNote(noteId, false) this.updateSingleNote(noteId, true)
} }
}) })
@ -264,10 +263,18 @@
//Close note event //Close note event
this.$bus.$on('close_active_note', ({noteId, modified}) => { 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') this.$store.dispatch('fetchAndUpdateUserTotals')
//Focus and animate if modified //Focus and animate if modified
this.updateSingleNote(parseInt(noteId), modified) this.updateSingleNote(noteId, modified)
}) })
this.$bus.$on('note_deleted', (noteId) => { this.$bus.$on('note_deleted', (noteId) => {
@ -312,35 +319,25 @@
}) })
}) })
//New note button pushes open note event //Reload page content - don't trigger if load is in progress
this.$bus.$on('open_note', noteId => {
this.openNote(noteId)
})
//Reload page content
this.$bus.$on('note_reload', () => { 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) window.addEventListener('scroll', this.onScroll)
//Close notes when back button is pressed //Close notes when back button is pressed
window.addEventListener('hashchange', this.hashChangeAction) // window.addEventListener('hashchange', this.hashChangeAction)
//update note on visibility change //update note on visibility change
document.addEventListener('visibilitychange', this.visibiltyChangeAction); // document.addEventListener('visibilitychange', this.visibiltyChangeAction);
}, },
beforeDestroy(){ beforeDestroy(){
window.removeEventListener('scroll', this.onScroll) 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('note_reload')
this.$bus.$off('close_active_note') this.$bus.$off('close_active_note')
@ -348,7 +345,6 @@
this.$bus.$off('note_deleted') this.$bus.$off('note_deleted')
this.$bus.$off('update_fast_filters') this.$bus.$off('update_fast_filters')
this.$bus.$off('update_search_term') this.$bus.$off('update_search_term')
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 //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 // this.$off() // Remove all event listeners
@ -356,11 +352,20 @@
}, },
mounted() { 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 //Loads initial batch and tags
this.reset() 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: { methods: {
toggleTitleView(){ toggleTitleView(){
@ -382,17 +387,9 @@
if(nodeClick == 'A'){ return } if(nodeClick == 'A'){ return }
} }
//1 note open //Open note if a link was not clicked
if(this.activeNoteId1 == null){ this.$router.push('/notes/open/'+id)
this.activeNoteId1 = id return
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')
}, },
toggleTagFilter(tagId){ toggleTagFilter(tagId){
@ -429,34 +426,6 @@
return 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){ visibiltyChangeAction(event){
//Fuck this shit, just use web sockets //Fuck this shit, just use web sockets
@ -484,8 +453,11 @@
let note = null let note = null
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){ if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
note = 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 //Lookup one note using passed in ID
const postData = { const postData = {
searchQuery: this.searchTerm, searchQuery: this.searchTerm,
@ -508,18 +480,13 @@
if(note && newNote){ 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 //go through each prop and update it with new values
Object.keys(newNote).forEach(prop => { Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop] note[prop] = newNote[prop]
}) })
//Push new note to front if its modified //Push new note to front if its modified or we want it to
if(focuseAndAnimate){ if( focuseAndAnimate || note.updated != newNote.updated ){
// Find note, in section, move to front // Find note, in section, move to front
Object.keys(this.noteSections).forEach( key => { Object.keys(this.noteSections).forEach( key => {
@ -536,6 +503,7 @@
this.$nextTick( () => { this.$nextTick( () => {
//Trigger close animation on note //Trigger close animation on note
this.$refs['note-'+noteId][0].justClosed() this.$refs['note-'+noteId][0].justClosed()
this.$refs['note-'+noteId][0].showWorking = false
}) })
} }
@ -547,9 +515,14 @@
//Trigger close animation on note //Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){ if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].justClosed() this.$refs['note-'+noteId][0].justClosed()
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 //Trigger section rebuild
this.rebuildNoteCategorise() this.rebuildNoteCategorise()
}) })
@ -610,7 +583,6 @@
//Perform search - or die //Perform search - or die
this.loadingInProgress = true this.loadingInProgress = true
// console.time('Fetch TitleCard Batch '+notesInNextLoad)
axios.post('/api/note/search', postData) axios.post('/api/note/search', postData)
.then(response => { .then(response => {
@ -735,7 +707,6 @@
this.fastFilters = {} this.fastFilters = {}
this.foundAttachments = [] //Remove all attachments this.foundAttachments = [] //Remove all attachments
this.$bus.$emit('reset_fast_filters')
this.updateFastFilters(5) //This loads notes this.updateFastFilters(5) //This loads notes
}, },

View File

@ -14,6 +14,11 @@
<div class="sixteen wide middle aligned column" v-if="quickNoteId > 0"> <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"> <div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
<i class="sync alternate reload icon"></i> <i class="sync alternate reload icon"></i>
New Scratch Pad New Scratch Pad
@ -46,10 +51,6 @@
<i class="folder open outline icon"></i> <i class="folder open outline icon"></i>
Files Files
</div> </div>
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">
<i class="file outline icon"></i>
Open Note
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,257 @@
<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>
<div class="ui segment">
<h3>Change Password</h3>
<p>Create a new scratch pad. Old scratch pad will turn into a normal note.</p>
<div class="ui compact basic button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
<i class="sync alternate reload icon"></i>
New Scratch Pad
</div>
<div v-if="showNewNoteConfirm" class="ui compact basic button shrinking" v-on:click="showNewNoteConfirm = false">
<i class="close icon"></i>
Cancel
</div>
<div v-if="showNewNoteConfirm" class="ui compact basic button shrinking" v-on:click="newQuickNote()">
<i class="green thumbs up icon"></i>
Confirm
</div>
</div>
<!-- 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 v-model="change1" type="password" placeholder="Current Password">
</div>
</div>
<div class="five wide column">
<label>New Password</label>
<div class="ui fluid input">
<input v-model="change2" type="password" placeholder="New Password">
</div>
</div>
<div class="six wide column">
<label>Rereat New Password</label>
<div class="ui fluid action input">
<input v-model="change3" type="password" placeholder="Repeat Password">
<div v-on:click="passwordChange()" class="ui button" :class="{'green':(change1.length > 0 && change2 == change3)}">
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" v-on:click="revokeAllSessions()">
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: '',
showNewNoteConfirm: false,
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
],
change1: '',
change2: '',
change3: '',
}
},
methods: {
newQuickNote(){
this.showNewNoteConfirm = true
axios.post('/api/quick-note/new')
.then( ({data}) => {
this.showNewNoteConfirm = false
this.$store.dispatch('fetchAndUpdateUserTotals')
this.$bus.$emit('notification', 'New Scratch Pad Created')
})
},
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
}
})
},
passwordChange(){
if(this.change1 == '' || this.change2 == '' || this.change3 == ''){
this.$bus.$emit('notification', 'All Password Fields Required')
return
}
if(this.change1 == this.change2){
this.$bus.$emit('notification', 'Old password matches new password')
return
}
if(this.change2 != this.change3){
this.$bus.$emit('notification', 'New Passwords do not match')
return
}
const postData = {
'currentPass':this.change1,
'newPass':this.change3
}
axios.post('/api/user/changepassword', postData)
.then(({data}) => {
if(data){
this.$bus.$emit('notification', 'Success: Password Changed')
this.change1 = ''
this.change2 = ''
this.change3 = ''
} else {
this.$bus.$emit('notification', 'Failed to change password')
this.change1 = ''
}
})
},
revokeAllSessions(){
axios.post('/api/user/revokesessions')
.then(({data}) => {
this.$bus.$emit('notification', 'All other active sessions revoked.')
})
}
}
}
</script>

View File

@ -4,38 +4,55 @@
<div class="sixteen wide column"></div> <div class="sixteen wide column"></div>
<div class="sixteen wide column" v-if="text.length > 0 || title.length > 0"> <div class="sixteen wide column" v-if="text.length > 0 || title.length > 0">
<div class="ui text container squire-box" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}"> <div class="ui text container">
<h1 v-if="title">{{title}}</h1> <div class="ui segment" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<div v-if="text" v-html="text"></div> <h1 v-if="title">{{title}}</h1>
<div v-if="text" v-html="text" class="squire-box"></div>
</div>
</div> </div>
</div> </div>
<div class="sixteen wide column" v-if="!$store.getters.getLoggedIn"> <div class="sixteen wide column" v-if="!$store.getters.getLoggedIn">
<div class="ui text container"> <div class="ui text container">
<h2 class="ui header">
<img class="small-logo" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo"> <div class="ui segment">
<div class="content">
Solid Scribe is an easy, free, secure Note App <div class="ui grid">
<div class="sub header"> <div class="three wide middle aligned center aligned column">
Encrypted notes, only readable by you. Unless you share them. <img class="small-logo" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
</div>
<div class="thirteen wide column">
<!-- header -->
<h2 class="ui header">
<div class="content">
Solid Scribe is an easy, free, secure Note App
<div class="sub header">
Encrypted notes, only readable by you. Unless you share them.
</div>
</div>
</h2>
<!-- buttons -->
<div class="ui grid">
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/login">
<i class="plug icon"></i>Sign Up
</router-link>
</div>
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/">
<i class="comment outline icon"></i>
Learn More
</router-link>
</div>
</div>
</div> </div>
</div> </div>
</h2>
<div class="ui grid">
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/login">
<i class="plug icon"></i>Sign Up
</router-link>
</div>
<div class="eight wide center aligned column">
<router-link class="ui compact green button" to="/">
<i class="comment outline icon"></i>
Learn More
</router-link>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -99,7 +116,7 @@
<style type="text/css" scoped> <style type="text/css" scoped>
.small-logo { .small-logo {
width: 30px; width: 100%;
height: auto; height: auto;
} }
</style> </style>

View File

@ -6,6 +6,7 @@ import Router from 'vue-router'
const HomePage = () => import(/* webpackChunkName: "HomePage" */ '@/pages/HomePage') const HomePage = () => import(/* webpackChunkName: "HomePage" */ '@/pages/HomePage')
const LoginPage = () => import(/* webpackChunkName: "LoginPage" */ '@/pages/LoginPage') const LoginPage = () => import(/* webpackChunkName: "LoginPage" */ '@/pages/LoginPage')
const HelpPage = () => import(/* webpackChunkName: "HelpPage" */ '@/pages/HelpPage') const HelpPage = () => import(/* webpackChunkName: "HelpPage" */ '@/pages/HelpPage')
const SettingsPage = () => import(/* webpackChunkName: "SettingsPage" */ '@/pages/SettingsPage')
const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/SharePage') const SharePage = () => import(/* webpackChunkName: "SharePage" */ '@/pages/SharePage')
const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage') const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/NotesPage')
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage') const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
@ -51,6 +52,12 @@ export default new Router({
name: 'Help', name: 'Help',
meta: {title:'Help'}, meta: {title:'Help'},
component: HelpPage component: HelpPage
},
{
path: '/settings',
name: 'Settings',
meta: {title:'Settings'},
component: SettingsPage
}, },
{ {
path: '/public/note/:id/:token', path: '/public/note/:id/:token',

View File

@ -85,6 +85,7 @@ export default new Vuex.Store({
Object.keys( themes[currentTheme] ).forEach( attribute => { Object.keys( themes[currentTheme] ).forEach( attribute => {
root.style.setProperty('--'+attribute, themes[currentTheme][attribute]) root.style.setProperty('--'+attribute, themes[currentTheme][attribute])
}) })
}, },
detectIsUserOnMobile(state){ detectIsUserOnMobile(state){

265
package-lock.json generated
View File

@ -49,6 +49,19 @@
"uri-js": "^4.2.2" "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": { "append-field": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" "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": { "base64-arraybuffer": {
"version": "0.1.5", "version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" "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": { "base64id": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz",
"integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==" "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": { "buffer-equal-constant-time": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" "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": { "buffer-from": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "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", "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
"integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" "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": { "camelize": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
@ -252,6 +308,29 @@
"parse5": "^3.0.1" "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": { "combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -394,6 +473,11 @@
"ms": "2.0.0" "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": { "delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "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": { "dns-prefetch-control": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" "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": { "encodeurl": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@ -708,6 +802,14 @@
"unpipe": "~1.0.0" "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": { "forever-agent": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@ -746,6 +848,11 @@
"is-property": "^1.0.2" "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": { "getpass": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@ -926,6 +1033,11 @@
"safer-buffer": ">= 2.1.2 < 3" "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": { "indexof": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
"integrity": "sha1-N9905DCg5HVQ/lSi3v4w2KzZX2U=" "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": { "is-property": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
@ -1040,6 +1157,15 @@
"safe-buffer": "^5.0.1" "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": { "lodash": {
"version": "4.17.15", "version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
@ -1257,6 +1383,27 @@
"ee-first": "1.1.1" "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": { "parse5": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", "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", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=" "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": { "path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "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", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" "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": { "process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=" "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": { "qs": {
"version": "6.5.2", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
@ -1407,6 +1585,16 @@
"lodash": "^4.17.15" "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": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -1465,6 +1653,11 @@
"send": "0.17.1" "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": { "setprototypeof": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "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": { "sqlstring": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz", "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", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" "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": { "string_decoder": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
@ -1643,6 +1854,14 @@
"safe-buffer": "~5.1.0" "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": { "to-array": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
@ -1755,6 +1974,21 @@
"isexe": "^2.0.0" "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": { "ws": {
"version": "7.2.1", "version": "7.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz", "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", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=" "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": { "yallist": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" "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": { "yeast": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",

View File

@ -21,9 +21,11 @@
"multer": "^1.4.2", "multer": "^1.4.2",
"mysql2": "^1.7.0", "mysql2": "^1.7.0",
"node-tesseract-ocr": "^1.0.0", "node-tesseract-ocr": "^1.0.0",
"qrcode": "^1.4.4",
"request": "^2.88.2", "request": "^2.88.2",
"request-promise": "^4.2.5", "request-promise": "^4.2.5",
"socket.io": "^2.3.0" "socket.io": "^2.3.0",
"speakeasy": "^2.0.0"
}, },
"_moduleAliases": { "_moduleAliases": {
"@root": ".", "@root": ".",

View File

@ -1,11 +1,13 @@
const db = require('@config/database') const db = require('@config/database')
const jwt = require('jsonwebtoken') const jwt = require('jsonwebtoken')
const cs = require('@helpers/CryptoString') const cs = require('@helpers/CryptoString')
const speakeasy = require('speakeasy')
let Auth = {} let Auth = {}
const tokenSecretKey = process.env.JSON_KEY const tokenSecretKey = process.env.JSON_KEY
//Creates session token
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => { Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -41,6 +43,7 @@ Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) =>
}) })
} }
//Decodes session token
Auth.decodeToken = (token, request = null) => { Auth.decodeToken = (token, request = null) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -120,6 +123,7 @@ Auth.decodeToken = (token, request = null) => {
} }
Auth.terminateSession = (sessionId) => { Auth.terminateSession = (sessionId) => {
return db.promise().query('DELETE from user_active_session WHERE session_id = ?', [sessionId]) return db.promise().query('DELETE from user_active_session WHERE session_id = ?', [sessionId])
} }
@ -130,6 +134,143 @@ Auth.deletAllLoginKeys = (userId) => {
return db.promise().query('DELETE FROM user_active_session WHERE user_hash = ?', [userHash]) 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 = () => { Auth.test = () => {
const testUserId = 22 const testUserId = 22

View File

@ -259,13 +259,15 @@ const printResults = true
let UserTest = require('@models/User') let UserTest = require('@models/User')
let NoteTest = require('@models/Note') let NoteTest = require('@models/Note')
let AuthTest = require('@helpers/Auth') let AuthTest = require('@helpers/Auth')
Auth.testTwoFactor()
Auth.test() Auth.test()
UserTest.keyPairTest('genMan15', '1', printResults) UserTest.keyPairTest('genMan23', '1', printResults)
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults)) .then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
.then( message => { .then( message => {
if(printResults) console.log(message) if(printResults) console.log(message)
}) })
// Test Area
//Test //Test

View File

@ -182,7 +182,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
const encryptedText = cs.encrypt(masterKey, salt, textObject) const encryptedText = cs.encrypt(masterKey, salt, textObject)
db.promise() 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) => { .then( (rows, fields) => {
const rawTextId = rows[0].insertId const rawTextId = rows[0].insertId
@ -515,7 +515,7 @@ Note.setPinned = (userId, noteId, pinnedBoolean) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const pinned = pinnedBoolean ? 1:0 const pinned = pinnedBoolean ? 1:0
const now = Math.round((+new Date)/1000) const now = (+new Date)
//Update other note attributes //Update other note attributes
return db.promise() return db.promise()
@ -532,7 +532,7 @@ Note.setArchived = (userId, noteId, archivedBoolead) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const archived = archivedBoolead ? 1:0 const archived = archivedBoolead ? 1:0
const now = Math.round((+new Date)/1000) const now = (+new Date)
//Update other note attributes //Update other note attributes
return db.promise() return db.promise()
@ -549,7 +549,7 @@ Note.setTrashed = (userId, noteId, trashedBoolean, masterKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const trashed = trashedBoolean ? 1:0 const trashed = trashedBoolean ? 1:0
const now = Math.round((+new Date)/1000) const now = (+new Date)
//Update other note attributes //Update other note attributes
return db.promise() return db.promise()
@ -1048,7 +1048,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
// Always prioritize pinned notes in searches. // Always prioritize pinned notes in searches.
//Default Sort, order by last updated //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 //Order by Last Created Date
if(fastFilters.lastCreated == 1){ if(fastFilters.lastCreated == 1){

View File

@ -10,19 +10,28 @@ QuickNote.get = (userId, masterKey) => {
db.promise() db.promise()
.query(` .query(`
SELECT note.id FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1 SELECT note.id FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1`, [userId])
`, [userId])
.then((rows, fields) => { .then((rows, fields) => {
//Quick Note is set, return note text //Quick Note is set, return note text
if(rows[0][0] != undefined){ if(rows[0][0] != undefined){
let noteId = rows[0][0].id let noteId = rows[0][0].id
Note.get(userId, noteId, masterKey) return resolve({'noteId':noteId})
.then( noteObject => {
return resolve(noteObject)
})
} else { } else {
return resolve(null) //Or create a new note and get the id
let finalId = null
return Note.create(userId, 'Scratch Pad', '', masterKey)
.then(insertedId => {
finalId = insertedId
db.promise().query('UPDATE note SET quick_note = 1 WHERE id = ? AND user_id = ?',[insertedId, userId])
.then((rows, fields) => {
return resolve({'noteId':finalId})
})
})
} }

View File

@ -5,22 +5,32 @@ const Note = require('@models/Note')
const db = require('@config/database') const db = require('@config/database')
const Auth = require('@helpers/Auth') const Auth = require('@helpers/Auth')
const cs = require('@helpers/CryptoString') const cs = require('@helpers/CryptoString')
const speakeasy = require('speakeasy')
let User = module.exports = {} let User = module.exports = {}
const version = '3.0.1' const version = '3.1.5'
//Login a user, if that user does not exist create them //Login a user, if that user does not exist create them
//Issues login token //Issues login token
User.login = (username, password) => { User.login = (username, password, authToken = null) => {
return new Promise((resolve, reject) => { 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() db.promise()
.query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName]) .query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName])
.then((rows, fields) => { .then((rows, fields) => {
//
// Login User // Login User
// //
if(rows[0].length == 1){ if(rows[0].length == 1){
@ -28,34 +38,76 @@ User.login = (username, password) => {
//Pull out user data from database results //Pull out user data from database results
const lookedUpUser = rows[0][0] const lookedUpUser = rows[0][0]
//hash the password and check for a match //Verify Token if set
// const salt = new Buffer(lookedUpUser.salt, 'binary') const tokenValidates = speakeasy.totp.verify({
const salt = Buffer.from(lookedUpUser.salt, 'binary') 'secret': lookedUpUser['two_fa_secret'],
crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){ 'encoding': 'base32',
if(delivered_key.toString('hex') === lookedUpUser.password){ 'token': authToken,
'window': 2
})
User.generateMasterKey(lookedUpUser.id, password) if(lookedUpUser.two_fa_enabled == 1 && !authToken){
.then( result => User.getMasterKey(lookedUpUser.id, password))
.then(masterKey => {
User.generateKeypair(lookedUpUser.id, masterKey) statusObject['verificationRequired'] = true
.then(({publicKey, privateKey}) => { statusObject['message'] = '2FA authentication required.'
//Passback a json web token return resolve(statusObject)
Auth.createToken(lookedUpUser.id, masterKey) }
.then(token => {
return resolve({ token: token, userId:lookedUpUser.id }) 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 { } 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) .catch(console.log)
@ -186,6 +238,12 @@ User.getCounts = (userId) => {
Object.assign(countTotals, rows[0][0]) //combine results Object.assign(countTotals, rows[0][0]) //combine results
return db.promise().query('SELECT id AS quickNote FROM note WHERE quick_note = 1 AND user_id = ?', [userId])
}).then( (rows, fields) => {
Object.assign(countTotals, rows[0][0]) //combine results
//Convert everything to an int or 0 //Convert everything to an int or 0
Object.keys(countTotals).forEach( key => { Object.keys(countTotals).forEach( key => {
const count = parseInt(countTotals[key]) const count = parseInt(countTotals[key])
@ -253,13 +311,12 @@ User.generateMasterKey = (userId, password) => {
} }
User.getMasterKey = (userId, password) => { User.getMasterKey = (userId, password) => {
if(!userId || !password){
reject('Need userId and password to fetch key')
}
return new Promise((resolve, reject) => { 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]) db.promise().query('SELECT * FROM user_key WHERE user_id = ? LIMIT 1', [userId])
.then((rows, fields) => { .then((rows, fields) => {
@ -375,6 +432,66 @@ User.getByUserName = (username) => {
}) })
} }
User.changePassword = (userId, oldPass, newPass) => {
return new Promise((resolve, reject) => {
User.getMasterKey(userId, oldPass)
.then(masterKey => {
User.getPrivateKey(userId, masterKey)
.then(privateKey => {
//If success, user has correct password
// Generate new master pass, encrypt with new password
// const masterPassword = cs.createSmallSalt()
const salt = cs.createSmallSalt()
const encryptedMasterPassword = cs.encrypt(newPass, salt, masterKey)
const encryptedPrivateKey = cs.encrypt(masterKey, salt, privateKey)
db.promise()
.query(
'UPDATE user_key SET salt = ?, `key` = ?, private_key_encrypted = ? WHERE user_id = ? LIMIT 1',
[salt, encryptedMasterPassword, encryptedPrivateKey, userId]
).then((r,f) => {
//Create login using password
let shasum = crypto.createHash('sha512') //Prepare Hash
const saltString = shasum.digest('hex')
const passwordSalt = Buffer.from(saltString, 'binary') //Generate Salt hash
const iterations = 25000
crypto.pbkdf2(newPass, passwordSalt, iterations, 512, 'sha512', function(err, delivered_key) {
const deliveredPass = delivered_key.toString('hex')
db.promise().query('UPDATE user SET password = ?, salt = ? WHERE id = ? LIMIT 1', [deliveredPass, passwordSalt, userId])
.then((r,f) => {
return resolve(true)
})
})
})
})
})
.catch(error => {
resolve(false)
})
})
}
User.revokeActiveSessions = (userId, sessionId) => {
return new Promise((resolve, reject) => {
const userHash = cs.hash(String(userId)).toString('base64')
db.promise().query('DELETE FROM user_active_session WHERE user_hash = ? AND session_id != ?', [userHash, sessionId])
.then((r,f) => {
resolve(true)
})
})
}
User.deleteUser = (userId, password) => { User.deleteUser = (userId, password) => {
//Verify user is correct by decryptig master key with password //Verify user is correct by decryptig master key with password
@ -414,6 +531,7 @@ User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
const randomUsername = Math.random().toString(36).substring(2, 15); const randomUsername = Math.random().toString(36).substring(2, 15);
const randomPassword = '1' const randomPassword = '1'
const secondPassword = '2'
User.register(testUserName, password) User.register(testUserName, password)
.then( ({ token, userId }) => { .then( ({ token, userId }) => {
@ -421,7 +539,7 @@ User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
if(printResults) console.log('Test: Register User '+testUserName+' - Pass') if(printResults) console.log('Test: Register User '+testUserName+' - Pass')
return User.getMasterKey(testUserId, password) return User.getMasterKey(testUserId, password)
}) })
.then(newMasterKey => { .then(newMasterKey => {
masterKey = newMasterKey masterKey = newMasterKey
@ -453,6 +571,26 @@ User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
if(printResults) console.log('Test: Login New User - Pass') if(printResults) console.log('Test: Login New User - Pass')
return User.changePassword(testUserId, randomPassword, secondPassword)
})
.then(passwordChangeResults => {
if(printResults) console.log('Test: Password Change - ', passwordChangeResults?'Pass':'Fail')
return User.login(testUserName, secondPassword)
})
.then(reLogin => {
if(printResults) console.log('Test: Login With new Password - Pass')
return User.getMasterKey(testUserId, secondPassword)
})
.then(newMasterKey => {
masterKey = newMasterKey
resolve({testUserId, masterKey}) resolve({testUserId, masterKey})
}) })
}) })

View File

@ -6,9 +6,8 @@ let QuickNote = require('@models/QuickNote');
let userId = null let userId = null
let masterKey = null let masterKey = null
// middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function setUserId (req, res, next) {
if(userId = req.headers.userId){ if(req.headers.userId){
userId = req.headers.userId userId = req.headers.userId
masterKey = req.headers.masterKey masterKey = req.headers.masterKey
} }

View File

@ -1,7 +1,8 @@
var express = require('express') var express = require('express')
var router = express.Router() var router = express.Router()
let User = require('@models/User'); const User = require('@models/User')
const Auth = require('@helpers/Auth')
const cs = require('@helpers/CryptoString') const cs = require('@helpers/CryptoString')
// middleware that is specific to this router // middleware that is specific to this router
@ -9,26 +10,14 @@ router.use(function timeLog (req, res, next) {
// console.log('Time: ', Date.now()) // console.log('Time: ', Date.now())
next() 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 // Login User
router.post('/login', function (req, res) { 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 => { .then( returnData => {
res.send(returnData) res.send(returnData)
}) })
.catch(e => {
res.send(false)
})
}) })
// Logout User // Logout User
router.post('/logout', function (req, res) { router.post('/logout', function (req, res) {
@ -39,10 +28,7 @@ router.post('/logout', function (req, res) {
}) })
}) })
// Register User
// Login User
router.post('/register', function (req, res) { router.post('/register', function (req, res) {
User.register(req.body.username, req.body.password) User.register(req.body.username, req.body.password)
@ -55,6 +41,24 @@ router.post('/register', function (req, res) {
}) })
}) })
// change password
router.post('/changepassword', function (req, res) {
User.changePassword(req.headers.userId, req.body.currentPass, req.body.newPass)
.then( returnData => {
res.send(returnData)
})
})
//Revoke all active session keys for user
router.post('/revokesessions', function(req, res) {
User.revokeActiveSessions(req.headers.userId, req.headers.sessionId)
.then( returnData => {
res.send(returnData)
})
})
// fetch counts of users notes // fetch counts of users notes
router.post('/totals', function (req, res) { router.post('/totals', function (req, res) {
@ -62,5 +66,30 @@ router.post('/totals', function (req, res) {
.then( countsObject => res.send( countsObject )) .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 module.exports = router