Compare commits

..

No commits in common. "63a73705707d2ff77cb222723bf5029af6d7e275" and "9681bd26d50babcc9cfdf16e3f1f8be58b4f479e" have entirely different histories.

8 changed files with 47 additions and 384 deletions

36
main.go
View File

@ -20,9 +20,9 @@ import (
_ "embed" _ "embed"
) )
const VERSION = "v11"; const VERSION = "v10";
const LISTEN_HOST = "0.0.0.0"; const LISTEN_HOST = "0.0.0.0";
const DB_SCHEMA = 13 const DB_SCHEMA = 12
var ( var (
flagPort int flagPort int
@ -68,7 +68,6 @@ func main() {// {{{
http.HandleFunc("/session/create", sessionCreate) http.HandleFunc("/session/create", sessionCreate)
http.HandleFunc("/session/retrieve", sessionRetrieve) http.HandleFunc("/session/retrieve", sessionRetrieve)
http.HandleFunc("/session/authenticate", sessionAuthenticate) http.HandleFunc("/session/authenticate", sessionAuthenticate)
http.HandleFunc("/user/password", userPassword)
http.HandleFunc("/node/tree", nodeTree) http.HandleFunc("/node/tree", nodeTree)
http.HandleFunc("/node/retrieve", nodeRetrieve) http.HandleFunc("/node/retrieve", nodeRetrieve)
http.HandleFunc("/node/create", nodeCreate) http.HandleFunc("/node/create", nodeCreate)
@ -202,37 +201,6 @@ func sessionAuthenticate(w http.ResponseWriter, r *http.Request) {// {{{
}) })
}// }}} }// }}}
func userPassword(w http.ResponseWriter, r *http.Request) {// {{{
var err error
var ok bool
var session Session
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
CurrentPassword string
NewPassword string
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
ok, err = session.UpdatePassword(req.CurrentPassword, req.NewPassword)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"CurrentPasswordOK": ok,
})
}// }}}
func nodeTree(w http.ResponseWriter, r *http.Request) {// {{{ func nodeTree(w http.ResponseWriter, r *http.Request) {// {{{
var err error var err error
var session Session var session Session

View File

@ -91,7 +91,7 @@ func (session *Session) Authenticate(username, password string) (authenticated b
FROM public.user FROM public.user
WHERE WHERE
username=$1 AND username=$1 AND
password=password_hash(SUBSTRING(password FROM 1 FOR 32), $2::bytea) password=$2
`, `,
username, username,
password, password,

View File

@ -1,34 +0,0 @@
/* Required for the gen_random_bytes function */
CREATE EXTENSION pgcrypto;
CREATE FUNCTION password_hash(salt_hex char(32), pass bytea)
RETURNS char(96)
LANGUAGE plpgsql
AS
$$
BEGIN
RETURN (
SELECT
salt_hex ||
encode(
sha256(
decode(salt_hex, 'hex') || /* salt in binary */
pass /* password */
),
'hex'
)
);
END;
$$;
/* Password has to be able to accommodate 96 characters instead of previous 64.
* It can't be char(96), because then the password would be padded to 96 characters. */
ALTER TABLE public."user" ALTER COLUMN "password" TYPE varchar(96) USING "password"::varchar;
/* Update all users with salted and hashed passwords */
UPDATE public.user
SET password = password_hash( encode(gen_random_bytes(16),'hex'), password::bytea);
/* After the password hashing, all passwords are now hex encoded 32 characters salt and 64 characters hash,
* and the varchar type is not longer necessary. */
ALTER TABLE public."user" ALTER COLUMN "password" TYPE char(96) USING "password"::varchar;

View File

@ -55,13 +55,6 @@ button {
border: 2px solid #000; border: 2px solid #000;
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.5); box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.5);
z-index: 1025; z-index: 1025;
width: min-content;
}
#menu .section {
padding: 16px 16px 0px 16px;
font-weight: bold;
white-space: nowrap;
color: #c84a37;
} }
#menu .item { #menu .item {
padding: 16px; padding: 16px;
@ -144,7 +137,7 @@ button {
header { header {
display: grid; display: grid;
grid-area: header; grid-area: header;
grid-template-columns: min-content 1fr repeat(4, min-content); grid-template-columns: min-content 1fr repeat(3, min-content);
align-items: center; align-items: center;
padding: 8px 0px; padding: 8px 0px;
color: #333c11; color: #333c11;
@ -168,13 +161,17 @@ header .name {
padding-left: 16px; padding-left: 16px;
font-size: 1.25em; font-size: 1.25em;
} }
header .search, header .add {
header .add, padding-right: 16px;
cursor: pointer;
}
header .add img {
cursor: pointer;
height: 24px;
}
header .keys { header .keys {
padding-right: 16px; padding-right: 16px;
} }
header .search img,
header .add img,
header .keys img { header .keys img {
cursor: pointer; cursor: pointer;
height: 24px; height: 24px;
@ -279,11 +276,6 @@ header .menu {
margin-bottom: 32px; margin-bottom: 32px;
font-size: 1.5em; font-size: 1.5em;
} }
#notes-version {
margin-top: 64px;
color: #888;
text-align: center;
}
#node-content.encrypted { #node-content.encrypted {
color: #a00; color: #a00;
} }
@ -546,19 +538,6 @@ header .menu {
#app.node.toggle-tree #tree { #app.node.toggle-tree #tree {
display: none; display: none;
} }
#profile-settings {
color: #333;
padding: 16px;
}
#profile-settings .passwords {
display: grid;
grid-template-columns: min-content 200px;
grid-gap: 8px 16px;
margin-bottom: 16px;
}
#profile-settings .passwords div {
white-space: nowrap;
}
@media only screen and (max-width: 932px) { @media only screen and (max-width: 932px) {
#app.node { #app.node {
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank"; grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank";

View File

@ -1,107 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="191.20831"
height="227.46098"
viewBox="0 0 50.590532 60.182387"
version="1.1"
id="svg8"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
sodipodi:docname="search.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient16049">
<stop
style="stop-color:#ffffff;stop-opacity:0.36503741;"
offset="0"
id="stop16045" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop16047" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient16049"
id="linearGradient16051"
x1="36.007183"
y1="8.6465521"
x2="12.402377"
y2="41.804993"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#5056d3"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="2.8284271"
inkscape:cx="153.08862"
inkscape:cy="145.664"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true"
showguides="false">
<sodipodi:guide
position="57.608417,-11.948636"
orientation="0.81915204,0.57357644"
id="guide5717"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<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" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-2.3771159,-1.8407145)">
<path
style="color:#000000;overflow:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:8.46667;stroke-linecap:round;stroke-dasharray:none;paint-order:markers stroke fill;stop-color:#000000"
d="m 38.808695,43.970762 9.925619,13.819002"
id="path2090" />
<path
id="circle4774"
style="color:#000000;overflow:visible;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.175;paint-order:markers stroke fill;stop-color:#000000"
d="M 25.660449,1.8407145 A 23.283335,23.283335 0 0 0 2.3771159,25.124048 23.283335,23.283335 0 0 0 25.660449,48.407381 23.283335,23.283335 0 0 0 48.944299,25.124048 23.283335,23.283335 0 0 0 25.660449,1.8407145 Z m 0,6.35 A 16.933332,16.933332 0 0 1 42.593783,25.124048 16.933332,16.933332 0 0 1 25.660449,42.057381 16.933332,16.933332 0 0 1 8.7276327,25.124048 16.933332,16.933332 0 0 1 25.660449,8.1907145 Z" />
<circle
style="color:#000000;overflow:visible;fill:url(#linearGradient16051);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.175;paint-order:markers stroke fill;stop-color:#000000"
id="path429"
cx="25.660707"
cy="25.123877"
r="16.933332" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -57,7 +57,7 @@ export class NodeUI extends Component {
case 'node': case 'node':
if(node.ID == 0) { if(node.ID == 0) {
page = html` page = html`
${children.length > 0 ? html`<div class="child-nodes">${children}</div><div id="notes-version">Notes version ${window._VERSION}</div>` : html``} ${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
` `
} else { } else {
let padlock = '' let padlock = ''
@ -86,10 +86,6 @@ export class NodeUI extends Component {
page = html`<${Keys} nodeui=${this} />` page = html`<${Keys} nodeui=${this} />`
break break
case 'profile-settings':
page = html`<${ProfileSettings} nodeui=${this} />`
break
case 'search': case 'search':
page = html`<${Search} nodeui=${this} />` page = html`<${Search} nodeui=${this} />`
break break
@ -104,7 +100,6 @@ export class NodeUI extends Component {
<header class="${modified}" onclick=${()=>this.saveNode()}> <header class="${modified}" onclick=${()=>this.saveNode()}>
<div class="tree"><img src="/images/${window._VERSION}/tree.svg" onclick=${()=>document.getElementById('app').classList.toggle('toggle-tree')} /></div> <div class="tree"><img src="/images/${window._VERSION}/tree.svg" onclick=${()=>document.getElementById('app').classList.toggle('toggle-tree')} /></div>
<div class="name">Notes</div> <div class="name">Notes</div>
<div class="search" onclick=${evt=>{ evt.stopPropagation(); this.showPage('search')}}><img src="/images/${window._VERSION}/search.svg" /></div>
<div class="add" onclick=${evt=>this.createNode(evt)}><img src="/images/${window._VERSION}/add.svg" /></div> <div class="add" onclick=${evt=>this.createNode(evt)}><img src="/images/${window._VERSION}/add.svg" /></div>
<div class="keys" onclick=${evt=>{ evt.stopPropagation(); this.showPage('keys')}}><img src="/images/${window._VERSION}/padlock.svg" /></div> <div class="keys" onclick=${evt=>{ evt.stopPropagation(); this.showPage('keys')}}><img src="/images/${window._VERSION}/padlock.svg" /></div>
<div class="menu" onclick=${evt=>this.showMenu(evt)}></div> <div class="menu" onclick=${evt=>this.showMenu(evt)}></div>
@ -218,11 +213,7 @@ export class NodeUI extends Component {
this.node.value.create(name, nodeID=>this.goToNode(nodeID)) this.node.value.create(name, nodeID=>this.goToNode(nodeID))
}//}}} }//}}}
saveNode() {//{{{ saveNode() {//{{{
let nodeContent = this.nodeContent.current let content = this.nodeContent.current.contentDiv.current.value
if(this.page.value != 'node' || nodeContent === null)
return
let content = nodeContent.contentDiv.current.value
this.node.value.setContent(content) this.node.value.setContent(content)
this.node.value.save(()=>this.props.app.nodeModified.value = false) this.node.value.save(()=>this.props.app.nodeModified.value = false)
}//}}} }//}}}
@ -556,14 +547,10 @@ class Menu extends Component {
return html` return html`
<div id="blackout" onclick=${()=>nodeui.menu.value = false}></div> <div id="blackout" onclick=${()=>nodeui.menu.value = false}></div>
<div id="menu"> <div id="menu">
<div class="section">Current note</div>
<div class="item" onclick=${()=>{ nodeui.renameNode(); nodeui.menu.value = false }}>Rename</div> <div class="item" onclick=${()=>{ nodeui.renameNode(); nodeui.menu.value = false }}>Rename</div>
<div class="item" onclick=${()=>{ nodeui.upload.value = true; nodeui.menu.value = false }}>Upload</div> <div class="item" onclick=${()=>{ nodeui.deleteNode(); nodeui.menu.value = false }}>Delete</div>
<div class="item " onclick=${()=>{ nodeui.showPage('node-properties'); nodeui.menu.value = false }}>Properties</div> <div class="item separator" onclick=${()=>{ nodeui.showPage('properties'); nodeui.menu.value = false }}>Properties</div>
<div class="item separator" onclick=${()=>{ nodeui.deleteNode(); nodeui.menu.value = false }}>Delete</div> <div class="item separator" onclick=${()=>{ nodeui.upload.value = true; nodeui.menu.value = false }}>Upload</div>
<div class="section">User</div>
<div class="item" onclick=${()=>{ nodeui.showPage('profile-settings'); nodeui.menu.value = false }}>Settings</div>
<div class="item" onclick=${()=>{ nodeui.logout(); nodeui.menu.value = false }}>Log out</div> <div class="item" onclick=${()=>{ nodeui.logout(); nodeui.menu.value = false }}>Log out</div>
</div> </div>
` `
@ -801,77 +788,4 @@ class Search extends Component {
}//}}} }//}}}
} }
class ProfileSettings extends Component {
render({ nodeui }, {}) {//{{{
return html`
<div id="profile-settings">
<h1>User settings</h1>
<h2>Password</h2>
<div class="passwords">
<div>Current</div>
<input type="password" id="current-password" placeholder="Current password" onkeydown=${evt=>this.keyHandler(evt)} />
<div>New</div>
<input type="password" id="new-password1" placeholder="Password" onkeydown=${evt=>this.keyHandler(evt)} />
<div>Repeat</div>
<input type="password" id="new-password2" placeholder="Repeat password" onkeydown=${evt=>this.keyHandler(evt)} />
</div>
<button onclick=${()=>this.updatePassword()}>Change password</button>
</div>`
}//}}}
componentDidMount() {//{{{
document.getElementById('current-password').focus()
}//}}}
keyHandler(evt) {//{{{
let handled = true
switch(evt.key.toUpperCase()) {
case 'ENTER':
this.updatePassword()
break
default:
handled = false
}
if(handled) {
evt.preventDefault()
evt.stopPropagation()
}
}//}}}
updatePassword() {//{{{
let curr_pass = document.getElementById('current-password').value
let pass1 = document.getElementById('new-password1').value
let pass2 = document.getElementById('new-password2').value
try {
if(pass1.length < 4) {
throw new Error('Password has to be at least 4 characters long')
}
if(pass1 != pass2) {
throw new Error(`Passwords don't match`)
}
window._app.current.request('/user/password', {
CurrentPassword: curr_pass,
NewPassword: pass1,
})
.then(res=>{
if(res.CurrentPasswordOK)
alert('Password is changed successfully')
else
alert('Current password is invalid')
})
} catch(err) {
alert(err.message)
}
}//}}}
}
// vim: foldmethod=marker // vim: foldmethod=marker

View File

@ -56,14 +56,6 @@ button {
border: 2px solid #000; border: 2px solid #000;
box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.5); box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.5);
z-index: 1025; z-index: 1025;
width: min-content;
.section {
padding: 16px 16px 0px 16px;
font-weight: bold;
white-space: nowrap;
color: @accent_3;
}
.item { .item {
padding: 16px; padding: 16px;
@ -162,7 +154,7 @@ button {
header { header {
display: grid; display: grid;
grid-area: header; grid-area: header;
grid-template-columns: min-content 1fr repeat(4, min-content); grid-template-columns: min-content 1fr repeat(3, min-content);
align-items: center; align-items: center;
padding: 8px 0px; padding: 8px 0px;
color: darken(@accent_1, 35%); color: darken(@accent_1, 35%);
@ -189,7 +181,17 @@ header {
font-size: 1.25em; font-size: 1.25em;
} }
.search, .add, .keys { .add {
padding-right: 16px;
cursor: pointer;
img {
cursor: pointer;
height: 24px;
}
}
.keys {
padding-right: 16px; padding-right: 16px;
img { img {
@ -322,12 +324,6 @@ header {
font-size: 1.5em; font-size: 1.5em;
} }
#notes-version {
margin-top: 64px;
color: #888;
text-align: center;
}
#node-content.encrypted { #node-content.encrypted {
color: #a00; color: #a00;
} }
@ -621,22 +617,6 @@ header {
} }
#profile-settings {
color: #333;
padding: 16px;
.passwords {
display: grid;
grid-template-columns: min-content 200px;
grid-gap: 8px 16px;
margin-bottom: 16px;
div {
white-space: nowrap;
}
}
}
@media only screen and (max-width: 932px) { @media only screen and (max-width: 932px) {
#app.node { #app.node {
.layout-crumbs(); .layout-crumbs();

37
user.go
View File

@ -1,37 +0,0 @@
package main
import (
// Standard
"database/sql"
)
func (session Session) UpdatePassword(currPass, newPass string) (ok bool, err error) {
var result sql.Result
var rowsAffected int64
result, err = db.Exec(`
UPDATE public.user
SET
password = password_hash(
/* salt in hex */
ENCODE(gen_random_bytes(16), 'hex'),
/* password */
$1::bytea
)
WHERE
id = $2 AND
password=password_hash(SUBSTRING(password FROM 1 FOR 32), $3::bytea)
RETURNING id
`,
newPass,
session.UserID,
currPass,
)
if rowsAffected, err = result.RowsAffected(); err != nil {
return
}
return rowsAffected > 0, nil
}