Keys unlocking/locking
This commit is contained in:
parent
f9dfc8835c
commit
c65f46a17d
34
key.go
Normal file
34
key.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// External
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
)
|
||||||
|
|
||||||
|
type Key struct {
|
||||||
|
ID int
|
||||||
|
UserID int `db:"user_id"`
|
||||||
|
Description string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session Session) Keys() (keys []Key, err error) {
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
if rows, err = db.Queryx(`SELECT * FROM crypto_key WHERE user_id=$1`, session.UserID); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
keys = []Key{}
|
||||||
|
for rows.Next() {
|
||||||
|
key := Key{}
|
||||||
|
if err = rows.StructScan(&key); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
25
main.go
25
main.go
@ -22,7 +22,7 @@ import (
|
|||||||
|
|
||||||
const VERSION = "v0.2.2";
|
const VERSION = "v0.2.2";
|
||||||
const LISTEN_HOST = "0.0.0.0";
|
const LISTEN_HOST = "0.0.0.0";
|
||||||
const DB_SCHEMA = 6
|
const DB_SCHEMA = 7
|
||||||
|
|
||||||
var (
|
var (
|
||||||
flagPort int
|
flagPort int
|
||||||
@ -76,6 +76,7 @@ func main() {// {{{
|
|||||||
http.HandleFunc("/node/delete", nodeDelete)
|
http.HandleFunc("/node/delete", nodeDelete)
|
||||||
http.HandleFunc("/node/upload", nodeUpload)
|
http.HandleFunc("/node/upload", nodeUpload)
|
||||||
http.HandleFunc("/node/download", nodeDownload)
|
http.HandleFunc("/node/download", nodeDownload)
|
||||||
|
http.HandleFunc("/key/retrieve", keyRetrieve)
|
||||||
http.HandleFunc("/ws", websocketHandler)
|
http.HandleFunc("/ws", websocketHandler)
|
||||||
http.HandleFunc("/", staticHandler)
|
http.HandleFunc("/", staticHandler)
|
||||||
|
|
||||||
@ -540,6 +541,28 @@ func nodeFiles(w http.ResponseWriter, r *http.Request) {// {{{
|
|||||||
})
|
})
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
|
func keyRetrieve(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
log.Println("/key/retrieve")
|
||||||
|
var err error
|
||||||
|
var session Session
|
||||||
|
|
||||||
|
if session, _, err = ValidateSession(r, true); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err := session.Keys()
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
"Keys": keys,
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
|
||||||
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
||||||
// Append index.html if needed for further reading of the file
|
// Append index.html if needed for further reading of the file
|
||||||
p := requestPath
|
p := requestPath
|
||||||
|
10
sql/0007.sql
Normal file
10
sql/0007.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE public.crypto_key (
|
||||||
|
id serial NOT NULL,
|
||||||
|
user_id int4 NOT NULL,
|
||||||
|
description varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"key" char(144) NOT NULL,
|
||||||
|
CONSTRAINT crypto_key_pk PRIMARY KEY (id),
|
||||||
|
CONSTRAINT crypto_key_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.crypto_key.key IS 'salt(16 bytes) + [key encrypted with pbkdf2(pass, salt)]';
|
@ -19,16 +19,8 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
color: #abc837;
|
margin-top: 0px;
|
||||||
}
|
font-size: 1em;
|
||||||
.layout-crumbs {
|
|
||||||
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank";
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
|
||||||
/* blank */
|
|
||||||
}
|
|
||||||
.layout-crumbs #tree {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
#blackout {
|
#blackout {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -113,10 +105,21 @@ h1 {
|
|||||||
#upload .files .progress.done {
|
#upload .files .progress.done {
|
||||||
color: #0a0;
|
color: #0a0;
|
||||||
}
|
}
|
||||||
|
#properties {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 1025;
|
||||||
|
}
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
grid-template-columns: min-content 1fr min-content min-content;
|
grid-template-columns: min-content 1fr repeat(3, min-content);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
color: #333c11;
|
color: #333c11;
|
||||||
@ -149,6 +152,13 @@ header .add {
|
|||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
header .keys {
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
header .keys img {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
header .menu {
|
header .menu {
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
@ -341,6 +351,34 @@ header .menu {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
#keys {
|
||||||
|
padding: 32px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
#keys .key-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
grid-gap: 12px 12px;
|
||||||
|
align-items: end;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
#keys .key-list .status {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#keys .key-list .status img {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
#keys .key-list .status .locked {
|
||||||
|
color: #a00;
|
||||||
|
}
|
||||||
|
#keys .key-list .status .unlocked {
|
||||||
|
color: #0a0;
|
||||||
|
}
|
||||||
|
#keys .key-list .description {
|
||||||
|
cursor: pointer;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
.layout-tree {
|
.layout-tree {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank";
|
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank";
|
||||||
@ -386,6 +424,36 @@ header .menu {
|
|||||||
.layout-crumbs #tree {
|
.layout-crumbs #tree {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.layout-keys {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "header" "keys";
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: min-content /* header */ 1fr;
|
||||||
|
/* blank */
|
||||||
|
color: #fff;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.layout-keys #crumbs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.layout-keys .child-nodes {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.layout-keys .node-name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.layout-keys .grow-wrap {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.layout-keys #file-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.layout-keys #tree {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.layout-keys #keys {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
#app {
|
#app {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank";
|
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank";
|
||||||
@ -396,7 +464,6 @@ header .menu {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
#app.toggle-tree {
|
#app.toggle-tree {
|
||||||
/* blank */
|
|
||||||
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank";
|
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank";
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
||||||
@ -405,12 +472,8 @@ header .menu {
|
|||||||
#app.toggle-tree #tree {
|
#app.toggle-tree #tree {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#app.toggle-tree #tree {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 932px) {
|
@media only screen and (max-width: 932px) {
|
||||||
#app {
|
#app {
|
||||||
/* blank */
|
|
||||||
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank";
|
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank";
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
||||||
@ -419,9 +482,6 @@ header .menu {
|
|||||||
#app #tree {
|
#app #tree {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#app #tree {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#app.toggle-tree {
|
#app.toggle-tree {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "header" "tree";
|
grid-template-areas: "header" "tree";
|
||||||
|
76
static/images/padlock-closed.svg
Normal file
76
static/images/padlock-closed.svg
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="824.2666"
|
||||||
|
height="800"
|
||||||
|
viewBox="0 0 218.0872 211.66667"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
|
||||||
|
sodipodi:docname="padlock-closed.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
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">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.5"
|
||||||
|
inkscape:cx="679"
|
||||||
|
inkscape:cy="-6"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="2556"
|
||||||
|
inkscape:window-height="1404"
|
||||||
|
inkscape:window-x="2560"
|
||||||
|
inkscape:window-y="16"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:showpageshadow="true"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d6d6d6"
|
||||||
|
showborder="true"
|
||||||
|
showguides="true" />
|
||||||
|
<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(-268.56613,-53.644456)">
|
||||||
|
<g
|
||||||
|
id="XMLID_509_"
|
||||||
|
transform="matrix(0.64141414,0,0,0.64141414,307.05737,53.644456)"
|
||||||
|
style="fill:#a02c2c">
|
||||||
|
<path
|
||||||
|
id="XMLID_510_"
|
||||||
|
d="m 65,330 h 200 c 8.284,0 15,-6.716 15,-15 V 145 c 0,-8.284 -6.716,-15 -15,-15 H 250 V 85 C 250,38.131 211.869,0 165,0 118.131,0 80,38.131 80,85 v 45 H 65 c -8.284,0 -15,6.716 -15,15 v 170 c 0,8.284 6.716,15 15,15 z M 180,234.986 V 255 c 0,8.284 -6.716,15 -15,15 -8.284,0 -15,-6.716 -15,-15 v -20.014 c -6.068,-4.565 -10,-11.824 -10,-19.986 0,-13.785 11.215,-25 25,-25 13.785,0 25,11.215 25,25 0,8.162 -3.932,15.421 -10,19.986 z M 110,85 c 0,-30.327 24.673,-55 55,-55 30.327,0 55,24.673 55,55 v 45 H 110 Z"
|
||||||
|
style="fill:#a02c2c" />
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
80
static/images/padlock-open.svg
Normal file
80
static/images/padlock-open.svg
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="824.2666"
|
||||||
|
height="800"
|
||||||
|
viewBox="0 0 218.0872 211.66667"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
|
||||||
|
sodipodi:docname="padlock-open.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
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">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.5"
|
||||||
|
inkscape:cx="679"
|
||||||
|
inkscape:cy="-6"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="2556"
|
||||||
|
inkscape:window-height="1404"
|
||||||
|
inkscape:window-x="2560"
|
||||||
|
inkscape:window-y="16"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:showpageshadow="true"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d6d6d6"
|
||||||
|
showborder="true"
|
||||||
|
showguides="true" />
|
||||||
|
<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(-268.56613,-53.644456)">
|
||||||
|
<g
|
||||||
|
id="g8389">
|
||||||
|
<path
|
||||||
|
id="path8097"
|
||||||
|
d="m 348.74929,265.31112 h 128.28283 c 5.31347,0 9.62121,-4.30774 9.62121,-9.62121 v -109.0404 c 0,-5.31348 -4.30774,-9.62122 -9.62121,-9.62122 h -9.62122 -109.0404 -9.62121 c -5.31348,0 -9.62121,4.30774 -9.62121,9.62122 v 109.0404 c 0,5.31347 4.30773,9.62121 9.62121,9.62121 z m 73.76263,-60.94332 v 12.83726 c 0,5.31348 -4.30774,9.62121 -9.62122,9.62121 -5.31347,0 -9.62121,-4.30773 -9.62121,-9.62121 V 204.3678 c -3.8921,-2.92806 -6.41414,-7.58408 -6.41414,-12.8193 0,-8.8419 7.19346,-16.03536 16.03535,-16.03536 8.8419,0 16.03536,7.19346 16.03536,16.03536 0,5.23522 -2.52204,9.89124 -6.41414,12.8193 z"
|
||||||
|
style="fill:#5aa02c;stroke-width:0.641414"
|
||||||
|
sodipodi:nodetypes="sssssccsssscssscsssc" />
|
||||||
|
<path
|
||||||
|
id="path8197"
|
||||||
|
d="m 377.60654,137.21538 v -28.86363 c 0,-30.062442 -24.45777,-54.520205 -54.52021,-54.520205 -30.06243,0 -54.5202,24.457763 -54.5202,54.520205 v 28.86363 m 19.24243,0 v -28.86363 c 0,-19.45217 15.82561,-35.277781 35.27777,-35.277781 19.45217,0 35.27778,15.825611 35.27778,35.277781 v 28.86363"
|
||||||
|
style="fill:#5aa02c;stroke-width:0.641414"
|
||||||
|
sodipodi:nodetypes="csssccsssc"
|
||||||
|
inkscape:transform-center-x="-44.498125" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
46
static/images/padlock.svg
Normal file
46
static/images/padlock.svg
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
height="800"
|
||||||
|
width="557.57574"
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
viewBox="0 0 229.99999 330"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="lock-svgrepo-com.svg"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs6955" /><sodipodi:namedview
|
||||||
|
id="namedview6953"
|
||||||
|
pagecolor="#efb591"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.34875"
|
||||||
|
inkscape:cx="157.924"
|
||||||
|
inkscape:cy="399.62929"
|
||||||
|
inkscape:window-width="2190"
|
||||||
|
inkscape:window-height="1404"
|
||||||
|
inkscape:window-x="1463"
|
||||||
|
inkscape:window-y="16"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Layer_1" />
|
||||||
|
<g
|
||||||
|
id="XMLID_509_"
|
||||||
|
transform="translate(-50)"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
<path
|
||||||
|
id="XMLID_510_"
|
||||||
|
d="m 65,330 h 200 c 8.284,0 15,-6.716 15,-15 V 145 c 0,-8.284 -6.716,-15 -15,-15 H 250 V 85 C 250,38.131 211.869,0 165,0 118.131,0 80,38.131 80,85 v 45 H 65 c -8.284,0 -15,6.716 -15,15 v 170 c 0,8.284 6.716,15 15,15 z M 180,234.986 V 255 c 0,8.284 -6.716,15 -15,15 -8.284,0 -15,-6.716 -15,-15 v -20.014 c -6.068,-4.565 -10,-11.824 -10,-19.986 0,-13.785 11.215,-25 25,-25 13.785,0 25,11.215 25,25 0,8.162 -3.932,15.421 -10,19.986 z M 110,85 c 0,-30.327 24.673,-55 55,-55 30.327,0 55,24.673 55,55 v 45 H 110 Z"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
@ -5,6 +5,8 @@
|
|||||||
<meta name="viewport" content="initial-scale=1.0, user-scalable=yes" />
|
<meta name="viewport" content="initial-scale=1.0, user-scalable=yes" />
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/main.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/main.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/login.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/login.css">
|
||||||
|
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/sjcl.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/css_reload.js"></script>
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
@ -16,12 +18,12 @@
|
|||||||
"preact/signals": "/js/{{ .VERSION }}/lib/signals/signals.mjs",
|
"preact/signals": "/js/{{ .VERSION }}/lib/signals/signals.mjs",
|
||||||
"htm": "/js/{{ .VERSION }}/lib/htm/htm.mjs",
|
"htm": "/js/{{ .VERSION }}/lib/htm/htm.mjs",
|
||||||
"session": "/js/{{ .VERSION }}/session.mjs",
|
"session": "/js/{{ .VERSION }}/session.mjs",
|
||||||
"node": "/js/{{ .VERSION }}/node.mjs"
|
"node": "/js/{{ .VERSION }}/node.mjs",
|
||||||
|
"key": "/js/{{ .VERSION }}/key.mjs",
|
||||||
|
"crypto": "/js/{{ .VERSION }}/crypto.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/css_reload.js"></script>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
69
static/js/crypto.mjs
Normal file
69
static/js/crypto.mjs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
export default class Crypto {
|
||||||
|
constructor(key) {
|
||||||
|
if(typeof key === 'string')
|
||||||
|
this.key = sjcl.codec.base64.toBits(base64_key)
|
||||||
|
else
|
||||||
|
this.key = key
|
||||||
|
|
||||||
|
this.aes = new sjcl.cipher.aes(this.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
static generate_key() {
|
||||||
|
return sjcl.random.randomWords(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
static pass_to_key(pass, salt = null) {
|
||||||
|
if(salt === null)
|
||||||
|
salt = sjcl.random.randomWords(4) // 128 bits (16 bytes)
|
||||||
|
let key = sjcl.misc.pbkdf2(pass, salt, 10000)
|
||||||
|
|
||||||
|
return {
|
||||||
|
salt,
|
||||||
|
key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(plaintext_data_in_bits, counter, return_encoded = true) {
|
||||||
|
// 8 bytes of random data, (1 word = 4 bytes) * 2
|
||||||
|
// with 8 bytes of byte encoded counter is used as
|
||||||
|
// IV to guarantee a non-repeated IV (which is a catastrophe).
|
||||||
|
// Assumes counter value is kept unique. Counter is taken from
|
||||||
|
// Postgres sequence.
|
||||||
|
let random_bits = sjcl.random.randomWords(2)
|
||||||
|
let iv_bytes = sjcl.codec.bytes.fromBits(random_bits)
|
||||||
|
for (let i = 0; i < 8; ++i) {
|
||||||
|
let mask = 0xffn << BigInt(i*8)
|
||||||
|
let counter_i_byte = (counter & mask) >> BigInt(i*8)
|
||||||
|
iv_bytes[15-i] = Number(counter_i_byte)
|
||||||
|
}
|
||||||
|
let iv = sjcl.codec.bytes.toBits(iv_bytes)
|
||||||
|
|
||||||
|
let encrypted = sjcl.mode['ccm'].encrypt(
|
||||||
|
this.aes,
|
||||||
|
plaintext_data_in_bits,
|
||||||
|
iv,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returning 16 bytes (4 words) IV + encrypted data.
|
||||||
|
if(return_encoded)
|
||||||
|
return sjcl.codec.base64.fromBits(
|
||||||
|
iv.concat(encrypted)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
return iv.concat(encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(encrypted_base64_data) {
|
||||||
|
try {
|
||||||
|
let encoded = sjcl.codec.base64.toBits(encrypted_base64_data)
|
||||||
|
let iv = encoded.slice(0, 4) // in words (4 bytes), not bytes
|
||||||
|
let encrypted_data = encoded.slice(4)
|
||||||
|
return sjcl.mode['ccm'].decrypt(this.aes, encrypted_data, iv)
|
||||||
|
} catch(err) {
|
||||||
|
if(err.message == `ccm: tag doesn't match`)
|
||||||
|
throw('Decryption failed')
|
||||||
|
else
|
||||||
|
throw(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
static/js/key.mjs
Normal file
113
static/js/key.mjs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import 'preact/devtools'
|
||||||
|
import { h, Component } from 'preact'
|
||||||
|
import htm from 'htm'
|
||||||
|
import Crypto from 'crypto'
|
||||||
|
const html = htm.bind(h)
|
||||||
|
|
||||||
|
export class Keys extends Component {
|
||||||
|
constructor(props) {//{{{
|
||||||
|
super(props)
|
||||||
|
this.retrieveKeys()
|
||||||
|
}//}}}
|
||||||
|
render({ nodeui }) {//{{{
|
||||||
|
let keys = nodeui.keys.value
|
||||||
|
.sort((a,b)=>{
|
||||||
|
if(a.description < b.description) return -1
|
||||||
|
if(a.description > b.description) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
.map(key=>
|
||||||
|
html`<${KeyComponent} key=${`key-${key.ID}`} model=${key} />`
|
||||||
|
)
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div id="keys">
|
||||||
|
<h1>Keys</h1>
|
||||||
|
<div class="key-list">
|
||||||
|
${keys}
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
}//}}}
|
||||||
|
|
||||||
|
retrieveKeys() {//{{{
|
||||||
|
window._app.current.request('/key/retrieve', {})
|
||||||
|
.then(res=>{
|
||||||
|
this.props.nodeui.keys.value = res.Keys.map(keyData=>new Key(keyData))
|
||||||
|
})
|
||||||
|
.catch(window._app.current.responseError)
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Key {
|
||||||
|
constructor(data) {//{{{
|
||||||
|
this.ID = data.ID
|
||||||
|
this.description = data.Description
|
||||||
|
this.unlockedKey = data.Key
|
||||||
|
this.key = null
|
||||||
|
|
||||||
|
let hex_key = window.sessionStorage.getItem(`key-${this.ID}`)
|
||||||
|
if(hex_key)
|
||||||
|
this.key = sjcl.codec.hex.toBits(hex_key)
|
||||||
|
}//}}}
|
||||||
|
status() {//{{{
|
||||||
|
if(this.key === null)
|
||||||
|
return 'locked'
|
||||||
|
return 'unlocked'
|
||||||
|
}//}}}
|
||||||
|
lock() {//{{{
|
||||||
|
this.key = null
|
||||||
|
window.sessionStorage.removeItem(`key-${this.ID}`)
|
||||||
|
}//}}}
|
||||||
|
unlock(password) {//{{{
|
||||||
|
let db = sjcl.codec.hex.toBits(this.unlockedKey)
|
||||||
|
let salt = db.slice(0, 4)
|
||||||
|
let pass_key = Crypto.pass_to_key(password, salt)
|
||||||
|
let crypto = new Crypto(pass_key.key)
|
||||||
|
this.key = crypto.decrypt(sjcl.codec.base64.fromBits(db.slice(4)))
|
||||||
|
window.sessionStorage.setItem(`key-${this.ID}`, sjcl.codec.hex.fromBits(this.key))
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KeyComponent extends Component {
|
||||||
|
render({ model }) {//{{{
|
||||||
|
let status = ''
|
||||||
|
switch(model.status()) {
|
||||||
|
case 'locked':
|
||||||
|
status = html`<div class="status locked"><img src="/images/${window._VERSION}/padlock-closed.svg" /></div>`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'unlocked':
|
||||||
|
status = html`<div class="status unlocked"><img src="/images/${window._VERSION}/padlock-open.svg" /></div>`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="status" onclick=${()=>this.toggle()}>${status}</div>
|
||||||
|
<div class="description" onclick=${()=>this.toggle()}>${model.description}</div>
|
||||||
|
`
|
||||||
|
}//}}}
|
||||||
|
toggle() {//{{{
|
||||||
|
if(this.props.model.status() == 'locked')
|
||||||
|
this.unlock()
|
||||||
|
else
|
||||||
|
this.lock()
|
||||||
|
}//}}}
|
||||||
|
lock() {//{{{
|
||||||
|
this.props.model.lock()
|
||||||
|
this.forceUpdate()
|
||||||
|
}//}}}
|
||||||
|
unlock() {//{{{
|
||||||
|
let pass = prompt("Password")
|
||||||
|
if(!pass)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.props.model.unlock(pass)
|
||||||
|
this.forceUpdate()
|
||||||
|
} catch(err) {
|
||||||
|
alert(err)
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// vim: foldmethod=marker
|
2574
static/js/lib/sjcl.js
Normal file
2574
static/js/lib/sjcl.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
import { h, Component, createRef } from 'preact'
|
import { h, Component, createRef } from 'preact'
|
||||||
import htm from 'htm'
|
import htm from 'htm'
|
||||||
import { signal } from 'preact/signals'
|
import { signal } from 'preact/signals'
|
||||||
|
import { Keys, Key } from 'key'
|
||||||
const html = htm.bind(h)
|
const html = htm.bind(h)
|
||||||
|
|
||||||
export class NodeUI extends Component {
|
export class NodeUI extends Component {
|
||||||
@ -9,8 +10,9 @@ export class NodeUI extends Component {
|
|||||||
this.menu = signal(false)
|
this.menu = signal(false)
|
||||||
this.node = signal(null)
|
this.node = signal(null)
|
||||||
this.nodeContent = createRef()
|
this.nodeContent = createRef()
|
||||||
this.upload = signal(false)
|
this.keys = signal([])
|
||||||
|
|
||||||
|
this.page = signal('node')
|
||||||
window.addEventListener('popstate', evt=>{
|
window.addEventListener('popstate', evt=>{
|
||||||
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
||||||
this.goToNode(evt.state.nodeID, true)
|
this.goToNode(evt.state.nodeID, true)
|
||||||
@ -35,8 +37,8 @@ export class NodeUI extends Component {
|
|||||||
).reverse())
|
).reverse())
|
||||||
|
|
||||||
let children = node.Children.sort((a,b)=>{
|
let children = node.Children.sort((a,b)=>{
|
||||||
if(a.Name.toLowerCase() > b.Name.toLowerCase()) return 1;
|
if(a.Name.toLowerCase() > b.Name.toLowerCase()) return 1
|
||||||
if(a.Name.toLowerCase() < b.Name.toLowerCase()) return -1;
|
if(a.Name.toLowerCase() < b.Name.toLowerCase()) return -1
|
||||||
return 0
|
return 0
|
||||||
}).map(child=>html`
|
}).map(child=>html`
|
||||||
<div class="child-node" onclick=${()=>this.goToNode(child.ID)}>${child.Name}</div>
|
<div class="child-node" onclick=${()=>this.goToNode(child.ID)}>${child.Name}</div>
|
||||||
@ -44,23 +46,46 @@ export class NodeUI extends Component {
|
|||||||
|
|
||||||
let modified = ''
|
let modified = ''
|
||||||
if(this.props.app.nodeModified.value)
|
if(this.props.app.nodeModified.value)
|
||||||
modified = 'modified';
|
modified = 'modified'
|
||||||
|
|
||||||
let upload = '';
|
|
||||||
if(this.upload.value)
|
|
||||||
upload = html`<${UploadUI} nodeui=${this} />`
|
|
||||||
|
|
||||||
let menu = '';
|
// Page to display
|
||||||
|
let page = ''
|
||||||
|
switch(this.page.value) {
|
||||||
|
case 'node':
|
||||||
|
if(node.ID > 0)
|
||||||
|
page = html`
|
||||||
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||||
|
<div class="node-name">${node.Name}</div>
|
||||||
|
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
|
||||||
|
<${NodeFiles} node=${this.node.value} />
|
||||||
|
`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'upload':
|
||||||
|
page = html`<${UploadUI} nodeui=${this} />`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'node-properties':
|
||||||
|
page = html`<${NodeProperties} nodeui=${this} />`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'keys':
|
||||||
|
page = html`<${Keys} nodeui=${this} />`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let menu = ''
|
||||||
if(this.menu.value)
|
if(this.menu.value)
|
||||||
upload = html`<${Menu} nodeui=${this} />`
|
menu = html`<${Menu} nodeui=${this} />`
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${menu}
|
${menu}
|
||||||
${upload}
|
|
||||||
<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="add" onclick=${evt=>this.createNode(evt)}>+</div>
|
<div class="add" onclick=${evt=>this.createNode(evt)}>+</div>
|
||||||
|
<div class="keys" onclick=${()=>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>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -68,14 +93,7 @@ export class NodeUI extends Component {
|
|||||||
<div class="crumbs">${crumbs}</crumbs>
|
<div class="crumbs">${crumbs}</crumbs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
${page}
|
||||||
|
|
||||||
${node.ID > 0 ? html`
|
|
||||||
<div class="node-name">${node.Name}</div>
|
|
||||||
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
|
|
||||||
` : html``}
|
|
||||||
|
|
||||||
<${NodeFiles} node=${this.node.value} />
|
|
||||||
`
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
componentDidMount() {//{{{
|
componentDidMount() {//{{{
|
||||||
@ -91,6 +109,27 @@ export class NodeUI extends Component {
|
|||||||
keyHandler(evt) {//{{{
|
keyHandler(evt) {//{{{
|
||||||
let handled = true
|
let handled = true
|
||||||
switch(evt.key.toUpperCase()) {
|
switch(evt.key.toUpperCase()) {
|
||||||
|
case 'E':
|
||||||
|
if(evt.shiftKey && evt.altKey)
|
||||||
|
this.showPage('keys')
|
||||||
|
else
|
||||||
|
handled = false
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'N':
|
||||||
|
if(evt.shiftKey && evt.altKey)
|
||||||
|
this.createNode()
|
||||||
|
else
|
||||||
|
handled = false
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'P':
|
||||||
|
if(evt.shiftKey && evt.altKey)
|
||||||
|
this.showPage('node-properties')
|
||||||
|
else
|
||||||
|
handled = false
|
||||||
|
break
|
||||||
|
|
||||||
case 'S':
|
case 'S':
|
||||||
if(evt.ctrlKey || (evt.shiftKey && evt.altKey))
|
if(evt.ctrlKey || (evt.shiftKey && evt.altKey))
|
||||||
this.saveNode()
|
this.saveNode()
|
||||||
@ -98,16 +137,10 @@ export class NodeUI extends Component {
|
|||||||
handled = false
|
handled = false
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'N':
|
|
||||||
if((evt.ctrlKey && evt.AltKey) || (evt.shiftKey && evt.altKey))
|
|
||||||
this.createNode()
|
|
||||||
else
|
|
||||||
handled = false
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'U':
|
case 'U':
|
||||||
if((evt.ctrlKey && evt.altKey) || (evt.shiftKey && evt.altKey))
|
if(evt.shiftKey && evt.altKey)
|
||||||
this.upload.value = true
|
this.showPage('upload')
|
||||||
else
|
else
|
||||||
handled = false
|
handled = false
|
||||||
|
|
||||||
@ -151,7 +184,7 @@ export class NodeUI extends Component {
|
|||||||
|
|
||||||
// Hide tree toggle, as this would be the next natural action to do manually anyway.
|
// Hide tree toggle, as this would be the next natural action to do manually anyway.
|
||||||
// At least in mobile mode.
|
// At least in mobile mode.
|
||||||
document.getElementById('app').classList.remove('toggle-tree');
|
document.getElementById('app').classList.remove('toggle-tree')
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
createNode(evt) {//{{{
|
createNode(evt) {//{{{
|
||||||
@ -184,6 +217,10 @@ export class NodeUI extends Component {
|
|||||||
this.menu.value = false
|
this.menu.value = false
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
|
showPage(pg) {//{{{
|
||||||
|
this.page.value = pg
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeContent extends Component {
|
class NodeContent extends Component {
|
||||||
@ -342,12 +379,12 @@ export class Node {
|
|||||||
})
|
})
|
||||||
.then(blob=>{
|
.then(blob=>{
|
||||||
let url = window.URL.createObjectURL(blob)
|
let url = window.URL.createObjectURL(blob)
|
||||||
let a = document.createElement('a');
|
let a = document.createElement('a')
|
||||||
a.href = url;
|
a.href = url
|
||||||
a.download = fname;
|
a.download = fname
|
||||||
document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox
|
document.body.appendChild(a) // we need to append the element to the dom -> otherwise it will not work in firefox
|
||||||
a.click();
|
a.click()
|
||||||
a.remove(); //afterwards we remove the element again
|
a.remove() //afterwards we remove the element again
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
}
|
}
|
||||||
@ -358,7 +395,8 @@ class Menu extends Component {
|
|||||||
<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="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 separator" onclick=${()=>{ nodeui.deleteNode(); nodeui.menu.value = false }}>Delete</div>
|
<div class="item" onclick=${()=>{ nodeui.deleteNode(); nodeui.menu.value = false }}>Delete</div>
|
||||||
|
<div class="item separator" onclick=${()=>{ nodeui.showPage('properties'); nodeui.menu.value = false }}>Properties</div>
|
||||||
<div class="item separator" onclick=${()=>{ nodeui.upload.value = true; nodeui.menu.value = false }}>Upload</div>
|
<div class="item separator" onclick=${()=>{ nodeui.upload.value = true; nodeui.menu.value = false }}>Upload</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>
|
||||||
@ -463,6 +501,19 @@ class UploadUI extends Component {
|
|||||||
request.send(formdata)
|
request.send(formdata)
|
||||||
}//}}}
|
}//}}}
|
||||||
}
|
}
|
||||||
|
class NodeProperties extends Component {
|
||||||
|
constructor(props) {//{{{
|
||||||
|
super(props)
|
||||||
|
}//}}}
|
||||||
|
render({ nodeui }) {//{{{
|
||||||
|
return html`
|
||||||
|
<div id="blackout" onclick=${()=>nodeui.properties.value = false}></div>
|
||||||
|
<div id="properties">
|
||||||
|
<h1>Node properties</h1>
|
||||||
|
<input type="checkbox" id="node-encrypted" /> <label for="node-encrypted">Encrypted</label>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
@ -23,29 +23,8 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: @accent_1;
|
margin-top: 0px;
|
||||||
}
|
font-size: 1em;
|
||||||
|
|
||||||
.layout-crumbs {
|
|
||||||
grid-template-areas:
|
|
||||||
"header"
|
|
||||||
"crumbs"
|
|
||||||
"child-nodes"
|
|
||||||
"name"
|
|
||||||
"content"
|
|
||||||
"files"
|
|
||||||
"blank"
|
|
||||||
;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows:
|
|
||||||
min-content /* header */
|
|
||||||
min-content /* crumbs */
|
|
||||||
min-content /* child-nodes */
|
|
||||||
min-content /* name */
|
|
||||||
min-content /* content */
|
|
||||||
min-content /* files */
|
|
||||||
1fr; /* blank */
|
|
||||||
#tree { display: none }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#blackout {
|
#blackout {
|
||||||
@ -145,10 +124,23 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#properties {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 1025;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
grid-template-columns: min-content 1fr min-content min-content;
|
grid-template-columns: min-content 1fr repeat(3, min-content);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
//background: @accent_1;
|
//background: @accent_1;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@ -186,6 +178,15 @@ header {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keys {
|
||||||
|
padding-right: 16px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
@ -415,6 +416,35 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#keys {
|
||||||
|
padding: 32px;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
.key-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
grid-gap: 12px 12px;
|
||||||
|
align-items: end;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
|
||||||
|
.status {
|
||||||
|
cursor: pointer;
|
||||||
|
img {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locked { color: #a00 }
|
||||||
|
.unlocked { color: #0a0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
cursor: pointer;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.layout-tree {// {{{
|
.layout-tree {// {{{
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
@ -479,6 +509,27 @@ header {
|
|||||||
1fr; /* blank */
|
1fr; /* blank */
|
||||||
#tree { display: none }
|
#tree { display: none }
|
||||||
}// }}}
|
}// }}}
|
||||||
|
.layout-keys {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"header"
|
||||||
|
"keys"
|
||||||
|
;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows:
|
||||||
|
min-content /* header */
|
||||||
|
1fr; /* blank */
|
||||||
|
color: #fff;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
#crumbs { display: none }
|
||||||
|
.child-nodes { display: none }
|
||||||
|
.node-name { display: none }
|
||||||
|
.grow-wrap { display: none }
|
||||||
|
#file-section { display: none }
|
||||||
|
#tree { display: none }
|
||||||
|
#keys { display: block }
|
||||||
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
.layout-tree();
|
.layout-tree();
|
||||||
|
Loading…
Reference in New Issue
Block a user