Wip
This commit is contained in:
parent
a76c093d44
commit
c255b58335
@ -21,6 +21,10 @@ type Config struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
Session struct {
|
||||
DaysValid int
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigRead(filename string) (config Config, err error) {
|
||||
|
157
main.go
157
main.go
@ -11,9 +11,10 @@ import (
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const VERSION = "v0.0.1";
|
||||
const VERSION = "v0.0.3";
|
||||
const LISTEN_HOST = "0.0.0.0";
|
||||
|
||||
var (
|
||||
@ -33,6 +34,10 @@ func init() {// {{{
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if false {
|
||||
time.Sleep(time.Second*1)
|
||||
}
|
||||
}// }}}
|
||||
func main() {// {{{
|
||||
var err error
|
||||
@ -53,6 +58,7 @@ func main() {// {{{
|
||||
http.HandleFunc("/session/create", sessionCreate)
|
||||
http.HandleFunc("/session/retrieve", sessionRetrieve)
|
||||
http.HandleFunc("/session/authenticate", sessionAuthenticate)
|
||||
http.HandleFunc("/node/retrieve", nodeRetrieve)
|
||||
http.HandleFunc("/ws", websocketHandler)
|
||||
http.HandleFunc("/", staticHandler)
|
||||
|
||||
@ -66,66 +72,6 @@ func cssUpdateHandler(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
log.Println("[BROADCAST] CSS updated")
|
||||
connectionManager.Broadcast(struct{ Ok bool; ID string; Op string }{ Ok: true, Op: "css_reload" })
|
||||
}// }}}
|
||||
func sessionCreate(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
session, err := NewSession()
|
||||
if err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
responseData(w, map[string]interface{}{
|
||||
"OK": true,
|
||||
"Session": session,
|
||||
})
|
||||
}// }}}
|
||||
func sessionRetrieve(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
var err error
|
||||
var uuid string
|
||||
|
||||
if uuid, err = sessionUUID(r); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
session := Session{ UUID: uuid }
|
||||
if err = session.Retrieve(); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
responseData(w, map[string]interface{}{
|
||||
"OK": true,
|
||||
"Session": session,
|
||||
})
|
||||
}// }}}
|
||||
func sessionAuthenticate(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
var err error
|
||||
var uuid string
|
||||
|
||||
if uuid, err = sessionUUID(r); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
session := Session{
|
||||
UUID: uuid,
|
||||
}
|
||||
|
||||
req := struct{ Username string; Password string }{}
|
||||
if err = request(r, &req); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = session.Authenticate(req.Username, req.Password); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
responseData(w, map[string]interface{}{
|
||||
"OK": true,
|
||||
"Session": session,
|
||||
})
|
||||
}// }}}
|
||||
func websocketHandler(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
var err error
|
||||
|
||||
@ -145,6 +91,7 @@ func staticHandler(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
// URLs with pattern /(css|images)/v1.0.0/foobar are stripped of the version.
|
||||
// To get rid of problems with cached content in browser on a new version release,
|
||||
// while also not disabling cache altogether.
|
||||
log.Printf("static: %s", r.URL.Path)
|
||||
rxp := regexp.MustCompile("^/(css|images|js|fonts)/v[0-9]+\\.[0-9]+\\.[0-9]+/(.*)$")
|
||||
if comp := rxp.FindStringSubmatch(r.URL.Path); comp != nil {
|
||||
r.URL.Path = fmt.Sprintf("/%s/%s", comp[1], comp[2])
|
||||
@ -154,6 +101,7 @@ func staticHandler(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
|
||||
// Everything else is run through the template system.
|
||||
// For now to get VERSION into files to fix caching.
|
||||
log.Printf("template: %s", r.URL.Path)
|
||||
tmpl, err := newTemplate(r.URL.Path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@ -168,6 +116,93 @@ func staticHandler(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
}
|
||||
}// }}}
|
||||
|
||||
func sessionCreate(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
log.Println("/session/create")
|
||||
session, err := CreateSession()
|
||||
if err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
responseData(w, map[string]interface{}{
|
||||
"OK": true,
|
||||
"Session": session,
|
||||
})
|
||||
}// }}}
|
||||
func sessionRetrieve(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
log.Println("/session/retrieve")
|
||||
var err error
|
||||
var found bool
|
||||
var session Session
|
||||
|
||||
if session, found, err = ValidateSession(r, false); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
responseData(w, map[string]interface{}{
|
||||
"OK": true,
|
||||
"Valid": found,
|
||||
"Session": session,
|
||||
})
|
||||
}// }}}
|
||||
func sessionAuthenticate(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
log.Println("/session/authenticate")
|
||||
var err error
|
||||
var session Session
|
||||
var authenticated bool
|
||||
|
||||
// Validate session
|
||||
if session, _, err = ValidateSession(r, true); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := struct{ Username string; Password string }{}
|
||||
if err = parseRequest(r, &req); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if authenticated, err = session.Authenticate(req.Username, req.Password); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
responseData(w, map[string]interface{}{
|
||||
"OK": true,
|
||||
"Authenticated": authenticated,
|
||||
"Session": session,
|
||||
})
|
||||
}// }}}
|
||||
|
||||
func nodeRetrieve(w http.ResponseWriter, r *http.Request) {// {{{
|
||||
log.Println("/node/retrieve")
|
||||
var err error
|
||||
var session Session
|
||||
|
||||
if session, _, err = ValidateSession(r, true); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := struct { ID int }{}
|
||||
if err = parseRequest(r, &req); err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
node, err := session.Node(req.ID)
|
||||
if err != nil {
|
||||
responseError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
responseData(w, map[string]interface{}{
|
||||
"OK": true,
|
||||
"Node": node,
|
||||
})
|
||||
}// }}}
|
||||
|
||||
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
||||
// Append index.html if needed for further reading of the file
|
||||
p := requestPath
|
||||
|
183
node.go
Normal file
183
node.go
Normal file
@ -0,0 +1,183 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// External
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// Standard
|
||||
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
ID int
|
||||
UserID int `db:"user_id"`
|
||||
ParentID int `db:"parent_id"`
|
||||
Name string
|
||||
Content string
|
||||
Children []Node
|
||||
Crumbs []Node
|
||||
Complete bool
|
||||
}
|
||||
|
||||
func (session Session) RootNode() (node Node, err error) {// {{{
|
||||
var rows *sqlx.Rows
|
||||
rows, err = db.Queryx(`
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
0 AS parent_id,
|
||||
name
|
||||
FROM node
|
||||
WHERE
|
||||
user_id = $1 AND
|
||||
parent_id IS NULL
|
||||
`,
|
||||
session.UserID,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
node.Name = "Start"
|
||||
node.UserID = session.UserID
|
||||
node.Complete = true
|
||||
node.Children = []Node{}
|
||||
node.Crumbs = []Node{}
|
||||
for rows.Next() {
|
||||
row := Node{}
|
||||
if err = rows.StructScan(&row); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
node.Children = append(node.Children, Node{
|
||||
ID: row.ID,
|
||||
UserID: row.UserID,
|
||||
ParentID: row.ParentID,
|
||||
Name: row.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}// }}}
|
||||
func (session Session) Node(nodeID int) (node Node, err error) {// {{{
|
||||
if nodeID == 0 {
|
||||
return session.RootNode()
|
||||
}
|
||||
|
||||
var rows *sqlx.Rows
|
||||
rows, err = db.Queryx(`
|
||||
WITH RECURSIVE recurse AS (
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
COALESCE(parent_id, 0) AS parent_id,
|
||||
name,
|
||||
content,
|
||||
0 AS level
|
||||
FROM node
|
||||
WHERE
|
||||
user_id = $1 AND
|
||||
id = $2
|
||||
|
||||
UNION
|
||||
|
||||
SELECT
|
||||
n.id,
|
||||
n.user_id,
|
||||
n.parent_id,
|
||||
n.name,
|
||||
'' AS content,
|
||||
r.level + 1 AS level
|
||||
FROM node n
|
||||
INNER JOIN recurse r ON n.parent_id = r.id AND r.level = 0
|
||||
WHERE
|
||||
n.user_id = $1
|
||||
)
|
||||
|
||||
SELECT * FROM recurse ORDER BY level ASC
|
||||
`,
|
||||
session.UserID,
|
||||
nodeID,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type resultRow struct {
|
||||
Node
|
||||
Level int
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
row := resultRow{}
|
||||
if err = rows.StructScan(&row); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if row.Level == 0 {
|
||||
node = Node{}
|
||||
node.Children = []Node{}
|
||||
node.ID = row.ID
|
||||
node.UserID = row.UserID
|
||||
node.ParentID = row.ParentID
|
||||
node.Name = row.Name
|
||||
node.Content = row.Content
|
||||
node.Complete = true
|
||||
}
|
||||
|
||||
if row.Level == 1 {
|
||||
node.Children = append(node.Children, Node{
|
||||
ID: row.ID,
|
||||
UserID: row.UserID,
|
||||
ParentID: row.ParentID,
|
||||
Name: row.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
node.Crumbs, err = session.NodeCrumbs(node.ID)
|
||||
|
||||
return
|
||||
}// }}}
|
||||
func (session Session) NodeCrumbs(nodeID int) (nodes []Node, err error) {// {{{
|
||||
var rows *sqlx.Rows
|
||||
rows, err = db.Queryx(`
|
||||
WITH RECURSIVE nodes AS (
|
||||
SELECT
|
||||
id,
|
||||
COALESCE(parent_id, 0) AS parent_id,
|
||||
name
|
||||
FROM node
|
||||
WHERE
|
||||
id = $1
|
||||
|
||||
UNION
|
||||
|
||||
SELECT
|
||||
n.id,
|
||||
COALESCE(n.parent_id, 0) AS parent_id,
|
||||
n.name
|
||||
FROM node n
|
||||
INNER JOIN nodes nr ON n.id = nr.parent_id
|
||||
)
|
||||
SELECT * FROM nodes
|
||||
`, nodeID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
nodes = []Node{}
|
||||
for rows.Next() {
|
||||
node := Node{}
|
||||
if err = rows.StructScan(&node); err != nil {
|
||||
return
|
||||
}
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
return
|
||||
}// }}}
|
||||
|
||||
// vim: foldmethod=marker
|
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
// Standard
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
@ -14,8 +13,8 @@ type Request struct {
|
||||
|
||||
func responseError(w http.ResponseWriter, err error) {
|
||||
res := map[string]interface{}{
|
||||
"ok": false,
|
||||
"error": err.Error(),
|
||||
"OK": false,
|
||||
"Error": err.Error(),
|
||||
}
|
||||
resJSON, _ := json.Marshal(res)
|
||||
|
||||
@ -29,17 +28,9 @@ func responseData(w http.ResponseWriter, data interface{}) {
|
||||
w.Write(resJSON)
|
||||
}
|
||||
|
||||
func request(r *http.Request, data interface{}) (err error) {
|
||||
func parseRequest(r *http.Request, data interface{}) (err error) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
defer r.Body.Close()
|
||||
err = json.Unmarshal(body, data)
|
||||
return
|
||||
}
|
||||
|
||||
func sessionUUID(r *http.Request) (string, error) {
|
||||
headers := r.Header["X-Session-Id"]
|
||||
if len(headers) > 0 {
|
||||
return headers[0], nil
|
||||
}
|
||||
return "", errors.New("Invalid session")
|
||||
}
|
||||
|
54
session.go
54
session.go
@ -4,6 +4,8 @@ import (
|
||||
// Standard
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -13,7 +15,7 @@ type Session struct {
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
func NewSession() (session Session, err error) {
|
||||
func CreateSession() (session Session, err error) {// {{{
|
||||
var rows *sql.Rows
|
||||
if rows, err = db.Query(`
|
||||
INSERT INTO public.session(uuid)
|
||||
@ -29,33 +31,60 @@ func NewSession() (session Session, err error) {
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}// }}}
|
||||
|
||||
func (session *Session) Retrieve() (err error) {
|
||||
func sessionUUID(r *http.Request) (string, error) {// {{{
|
||||
headers := r.Header["X-Session-Id"]
|
||||
if len(headers) > 0 {
|
||||
return headers[0], nil
|
||||
}
|
||||
return "", errors.New("Invalid session")
|
||||
}// }}}
|
||||
func ValidateSession(r *http.Request, notFoundIsError bool) (session Session, found bool, err error) {// {{{
|
||||
var uuid string
|
||||
if uuid, err = sessionUUID(r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
session.UUID = uuid
|
||||
if found, err = session.Retrieve(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if notFoundIsError && !found {
|
||||
err = errors.New("Invalid session")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}// }}}
|
||||
|
||||
func (session *Session) Retrieve() (found bool, err error) {// {{{
|
||||
var rows *sql.Rows
|
||||
if rows, err = db.Query(`
|
||||
SELECT
|
||||
uuid, user_id, created
|
||||
FROM public.session
|
||||
WHERE
|
||||
uuid = $1
|
||||
uuid = $1 AND
|
||||
created + $2::interval >= NOW()
|
||||
`,
|
||||
session.UUID,
|
||||
fmt.Sprintf("%d days", config.Session.DaysValid),
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
found = false
|
||||
if rows.Next() {
|
||||
found = true
|
||||
rows.Scan(&session.UUID, &session.UserID, &session.Created)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (session *Session) Authenticate(username, password string) (err error) {
|
||||
authenticated := false
|
||||
|
||||
}// }}}
|
||||
func (session *Session) Authenticate(username, password string) (authenticated bool, err error) {// {{{
|
||||
var rows *sql.Rows
|
||||
if rows, err = db.Query(`
|
||||
SELECT id
|
||||
@ -81,9 +110,10 @@ func (session *Session) Authenticate(username, password string) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = errors.New("Invalid authentication")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}// }}}
|
||||
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
28
sql/0001.sql
Normal file
28
sql/0001.sql
Normal file
@ -0,0 +1,28 @@
|
||||
CREATE TABLE public."user" (
|
||||
id SERIAL NOT NULL,
|
||||
username VARCHAR(64) NOT NULL DEFAULT ''::character varying,
|
||||
"password" VARCHAR(64) NOT NULL DEFAULT ''::character varying,
|
||||
last_login TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT newtable_pk PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE public."session" (
|
||||
uuid UUID NOT NULL,
|
||||
user_id INT4 NULL,
|
||||
created TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT session_pk PRIMARY KEY (uuid),
|
||||
CONSTRAINT user_session_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE public.node (
|
||||
id SERIAL NOT NULL,
|
||||
user_id INT4 NOT NULL,
|
||||
parent_id INT4 NULL,
|
||||
"name" VARCHAR(256) NOT NULL DEFAULT '',
|
||||
"content" TEXT NOT NULL DEFAULT '',
|
||||
CONSTRAINT name_length CHECK (LENGTH(TRIM(name)) > 0),
|
||||
CONSTRAINT node_pk PRIMARY KEY (id),
|
||||
CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT node_fk FOREIGN KEY (parent_id) REFERENCES public.node(id) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
);
|
||||
|
@ -25,3 +25,10 @@
|
||||
font-size: 0.8em;
|
||||
align-self: center;
|
||||
}
|
||||
#login .auth-failed {
|
||||
margin-top: 32px;
|
||||
color: #a00;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
@ -3,13 +3,60 @@ body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
font-family: 'Liberation Mono', monospace;
|
||||
font-size: 16pt;
|
||||
font-size: 14pt;
|
||||
background-color: #494949;
|
||||
}
|
||||
h1 {
|
||||
color: #ecbf00;
|
||||
}
|
||||
#app {
|
||||
padding: 32px;
|
||||
color: #fff;
|
||||
}
|
||||
.crumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 16px;
|
||||
background: #ecbf00;
|
||||
color: #000;
|
||||
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.crumbs .crumb {
|
||||
margin-right: 8px;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.crumbs .crumb:after {
|
||||
content: ">";
|
||||
margin-left: 8px;
|
||||
color: #a08100;
|
||||
}
|
||||
.crumbs .crumb:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.crumbs .crumb:last-child:after {
|
||||
content: '';
|
||||
margin-left: 0px;
|
||||
}
|
||||
.child-nodes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 16px 16px 0px 16px;
|
||||
background-color: #3c3c3c;
|
||||
box-shadow: 0px 0px 16px -4px rgba(0, 0, 0, 0.55) inset;
|
||||
}
|
||||
.child-nodes .child-node {
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: #292929;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.node-name {
|
||||
padding: 16px;
|
||||
}
|
||||
.node-content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
@ -17,7 +17,8 @@
|
||||
"@preact/signals-core": "/js/{{ .VERSION }}/lib/signals/signals-core.mjs",
|
||||
"preact/signals": "/js/{{ .VERSION }}/lib/signals/signals.mjs",
|
||||
"htm": "/js/{{ .VERSION }}/lib/htm/htm.mjs",
|
||||
"session": "/js/{{ .VERSION }}/session.mjs"
|
||||
"session": "/js/{{ .VERSION }}/session.mjs",
|
||||
"node": "/js/{{ .VERSION }}/node.mjs"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'preact/debug'
|
||||
import 'preact/devtools'
|
||||
//import { signal } from 'preact/signals'
|
||||
import { signal } from 'preact/signals'
|
||||
import { h, Component, render, createRef } from 'preact'
|
||||
import htm from 'htm'
|
||||
import { Session } from 'session'
|
||||
import { NodeUI } from 'node'
|
||||
const html = htm.bind(h)
|
||||
|
||||
class App extends Component {
|
||||
@ -17,18 +17,19 @@ class App extends Component {
|
||||
|
||||
this.session = new Session(this)
|
||||
this.session.initialize()
|
||||
this.login = createRef()
|
||||
this.nodeUI = createRef()
|
||||
}//}}}
|
||||
render() {//{{{
|
||||
if(!this.session.initialized) {
|
||||
return false;
|
||||
//return html`<div>Validating session</div>`
|
||||
return html`<div>Validating session</div>`
|
||||
}
|
||||
|
||||
if(!this.session.authenticated()) {
|
||||
return html`<${Login} />`
|
||||
return html`<${Login} ref=${this.login} />`
|
||||
}
|
||||
|
||||
return html`Welcome`;
|
||||
return html`<${NodeUI} app=${this} ref=${this.nodeUI} />`
|
||||
}//}}}
|
||||
|
||||
wsLoop() {//{{{
|
||||
@ -40,7 +41,7 @@ class App extends Component {
|
||||
}, 1000)
|
||||
}//}}}
|
||||
wsConnect() {//{{{
|
||||
this.websocket = new WebSocket(`wss://notes.ahall.se/ws`)
|
||||
this.websocket = new WebSocket(`ws://192.168.11.60:1371/ws`)
|
||||
this.websocket.onopen = evt=>this.wsOpen(evt)
|
||||
this.websocket.onmessage = evt=>this.wsMessage(evt)
|
||||
this.websocket.onerror = evt=>this.wsError(evt)
|
||||
@ -76,8 +77,8 @@ class App extends Component {
|
||||
return
|
||||
}
|
||||
|
||||
if(app !== undefined && app.hasOwnProperty('error')) {
|
||||
alert(app.error)
|
||||
if(app !== undefined && app.hasOwnProperty('Error')) {
|
||||
alert(app.Error)
|
||||
return
|
||||
}
|
||||
|
||||
@ -91,7 +92,6 @@ class App extends Component {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
console.log(this.session)
|
||||
if(this.session.UUID !== '')
|
||||
headers['X-Session-Id'] = this.session.UUID
|
||||
|
||||
@ -108,8 +108,9 @@ class App extends Component {
|
||||
})
|
||||
.then(json=>{
|
||||
// An application level error occured
|
||||
if(!json.OK)
|
||||
if(!json.OK) {
|
||||
return reject({app: json})
|
||||
}
|
||||
return resolve(json)
|
||||
})
|
||||
.catch(err=>reject({comm: err}))
|
||||
@ -126,13 +127,28 @@ class App extends Component {
|
||||
}
|
||||
|
||||
class Login extends Component {
|
||||
render() {//{{{
|
||||
constructor() {//{{{
|
||||
super()
|
||||
this.authentication_failed = signal(false)
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
}//}}}
|
||||
render({}, { username, password }) {//{{{
|
||||
console.log('login render')
|
||||
|
||||
let authentication_failed = html``;
|
||||
if(this.authentication_failed.value)
|
||||
authentication_failed = html`<div class="auth-failed">Authentication failed</div>`;
|
||||
|
||||
return html`
|
||||
<div id="login">
|
||||
<h1>Notes</h1>
|
||||
<input id="username" type="text" placeholder="Username" onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} />
|
||||
<input id="password" type="password" placeholder="Password" onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} />
|
||||
<input id="username" type="text" placeholder="Username" value=${username} oninput=${evt=>this.setState({ username: evt.target.value})} onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} />
|
||||
<input id="password" type="password" placeholder="Password" value=${password} oninput=${evt=>this.setState({ password: evt.target.value})} onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} />
|
||||
<button onclick=${()=>this.login()}>Login</button>
|
||||
${authentication_failed}
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
@ -149,6 +165,27 @@ class Login extends Component {
|
||||
|
||||
// Init{{{
|
||||
//let urlParams = new URLSearchParams(window.location.search)
|
||||
|
||||
/*
|
||||
async function debug(type, context, data) {
|
||||
await fetch("https://msg.kon-it.se/log", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
systemId: 12,
|
||||
type,
|
||||
context,
|
||||
data: JSON.stringify(data, null, 4),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
window.onerror = (event, source, lineno, colon, error) => {
|
||||
debug('Notes', 'error', {
|
||||
event, source, lineno, colon, error
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
window._app = createRef()
|
||||
window._resourceModels = []
|
||||
render(html`<${App} ref=${window._app} />`, document.getElementById('app'))
|
||||
|
89
static/js/node.mjs
Normal file
89
static/js/node.mjs
Normal file
@ -0,0 +1,89 @@
|
||||
import { h, Component } from 'preact'
|
||||
import htm from 'htm'
|
||||
import { signal } from 'preact/signals'
|
||||
const html = htm.bind(h)
|
||||
|
||||
export class NodeUI extends Component {
|
||||
constructor() {//{{{
|
||||
super()
|
||||
this.node = signal(null)
|
||||
window.addEventListener('popstate', evt=>{
|
||||
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
||||
this.goToNode(evt.state.nodeID, true)
|
||||
else
|
||||
this.goToNode(0, true)
|
||||
})
|
||||
}//}}}
|
||||
render() {//{{{
|
||||
if(this.node.value === null)
|
||||
return
|
||||
|
||||
let node = this.node.value
|
||||
|
||||
let crumbs = [
|
||||
html`<div class="crumb" onclick=${()=>this.goToNode(0)}>Start</div>`
|
||||
]
|
||||
|
||||
crumbs = crumbs.concat(node.Crumbs.slice(1).map(node=>
|
||||
html`<div class="crumb" onclick=${()=>this.goToNode(node.ID)}>${node.Name}</div>`
|
||||
).reverse())
|
||||
|
||||
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;
|
||||
return 0
|
||||
}).map(child=>html`
|
||||
<div class="child-node" onclick=${()=>this.goToNode(child.ID)}>${child.Name}</div>
|
||||
`)
|
||||
|
||||
return html`
|
||||
${node.ID > 0 ? html`<div class="crumbs">${crumbs}</crumbs>` : html``}
|
||||
<div class="node-name">${node.Name}</div>
|
||||
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||
<div class="node-content">${node.Content}</div>
|
||||
`
|
||||
}//}}}
|
||||
componentDidMount() {//{{{
|
||||
let root = new Node(this.props.app, 0)
|
||||
root.retrieve(node=>{
|
||||
this.node.value = node
|
||||
})
|
||||
}//}}}
|
||||
goToNode(nodeID, dontPush) {//{{{
|
||||
if(!dontPush)
|
||||
history.pushState({ nodeID }, '', `#${nodeID}`)
|
||||
let node = new Node(this.props.app, nodeID)
|
||||
node.retrieve(node=>{
|
||||
this.node.value = node
|
||||
})
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class Node {
|
||||
constructor(app, nodeID) {//{{{
|
||||
this.app = app
|
||||
this.ID = nodeID
|
||||
this.ParentID = 0
|
||||
this.UserID = 0
|
||||
this.Name = ''
|
||||
this.Content = ''
|
||||
this.Children = []
|
||||
this.Crumbs = []
|
||||
}//}}}
|
||||
|
||||
retrieve(callback) {//{{{
|
||||
this.app.request('/node/retrieve', { ID: this.ID })
|
||||
.then(res=>{
|
||||
this.ParentID = res.Node.ParentID
|
||||
this.UserID = res.Node.UserID
|
||||
this.Name = res.Node.Name
|
||||
this.Content = res.Node.Content
|
||||
this.Children = res.Node.Children
|
||||
this.Crumbs = res.Node.Crumbs
|
||||
callback(this)
|
||||
})
|
||||
.catch(this.app.responseError)
|
||||
}//}}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
@ -9,37 +9,61 @@ export class Session {
|
||||
initialize() {//{{{
|
||||
// Retrieving the stored session UUID, if any.
|
||||
// If one found, validate with server.
|
||||
let uuid = window.localStorage.getItem("session.UUID")
|
||||
|
||||
|
||||
// If the browser doesn't know anything about a session,
|
||||
// a call to /session/create is necessary to retrieve a session UUID.
|
||||
let uuid= window.localStorage.getItem("session.UUID")
|
||||
if(uuid === null) {
|
||||
this.create()
|
||||
} else {
|
||||
this.UUID = uuid
|
||||
this.app.request('/session/retrieve', {})
|
||||
.then(res=>{
|
||||
this.UserID = res.Session.UserID
|
||||
return
|
||||
}
|
||||
|
||||
// When loading the page, the web app needs to know session information
|
||||
// such as user ID and possibly some state to render properly.
|
||||
//
|
||||
// A call to /session/retrieve with a session UUID validates that the
|
||||
// session is still valid and returns all session information.
|
||||
this.UUID = uuid
|
||||
this.app.request('/session/retrieve', {})
|
||||
.then(res=>{
|
||||
if(res.Valid) {
|
||||
// Session exists on server.
|
||||
// Not necessarily authenticated.
|
||||
this.UserID = res.Session.UserID // could be 0
|
||||
this.initialized = true
|
||||
this.app.forceUpdate()
|
||||
})
|
||||
.catch(this.app.responseError)
|
||||
}
|
||||
} else {
|
||||
// Session has probably expired. A new is required.
|
||||
this.create()
|
||||
}
|
||||
})
|
||||
.catch(this.app.responseError)
|
||||
}//}}}
|
||||
create() {//{{{
|
||||
this.app.request('/session/create', {})
|
||||
.then(res=>{
|
||||
this.UUID = res.Session.UUID
|
||||
window.localStorage.setItem('session.UUID', this.UUID)
|
||||
this.initialized = true
|
||||
this.app.forceUpdate()
|
||||
})
|
||||
.catch(this.responseError)
|
||||
}//}}}
|
||||
authenticate(username, password) {//{{{
|
||||
this.app.login.current.authentication_failed.value = false
|
||||
|
||||
this.app.request('/session/authenticate', {
|
||||
username,
|
||||
password,
|
||||
})
|
||||
.then(res=>{
|
||||
this.UserID = res.Session.UserID
|
||||
this.app.forceUpdate()
|
||||
if(res.Authenticated) {
|
||||
this.UserID = res.Session.UserID
|
||||
this.app.forceUpdate()
|
||||
} else {
|
||||
this.app.login.current.authentication_failed.value = true
|
||||
}
|
||||
})
|
||||
.catch(this.app.responseError)
|
||||
}//}}}
|
||||
|
@ -30,4 +30,12 @@
|
||||
font-size: 0.8em;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.auth-failed {
|
||||
margin-top: 32px;
|
||||
color: #a00;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ html, body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
font-family: 'Liberation Mono', monospace;
|
||||
font-size: 16pt;
|
||||
font-size: @fontsize;
|
||||
|
||||
background-color: @background;
|
||||
}
|
||||
@ -14,6 +14,63 @@ h1 {
|
||||
}
|
||||
|
||||
#app {
|
||||
padding: 32px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.crumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 16px;
|
||||
background: @accent_1;
|
||||
color: #000;
|
||||
box-shadow: 0px 2px 8px 0px rgba(0,0,0,0.4);
|
||||
|
||||
.crumb {
|
||||
margin-right: 8px;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.crumb:after {
|
||||
content: ">";
|
||||
margin-left: 8px;
|
||||
color: darken(@accent_1, 15%);
|
||||
}
|
||||
|
||||
.crumb:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.crumb:last-child:after {
|
||||
content: '';
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.child-nodes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 16px 16px 0px 16px;
|
||||
background-color: darken(@background, 5%);
|
||||
box-shadow: 0px 0px 16px -4px rgba(0,0,0,0.55) inset;
|
||||
|
||||
.child-node {
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: darken(@background, 12.5%);
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.node-name {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
@ -1,2 +1,4 @@
|
||||
@fontsize: 14pt;
|
||||
@background: #494949;
|
||||
@accent_1: #ecbf00;
|
||||
@accent_2: #abc837;
|
||||
|
Loading…
Reference in New Issue
Block a user