all repos — felt @ 464a159354a2ab4d0008584af1f18e37b453d8be

virtual tabletop for dungeons and dragons (and similar) using Go, MongoDB, and websockets

starting admin middleware
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmOj4NwACgkQO3+8IhRO
Y5hxRw//dkfOypSMzSL3NOJmcsxgSuRp/gbwujEF962aDGQ2h/NKa+c0BiVxeIP8
Rv9fi/CxYOv118hGff/hlAF/CoplJy1QXKgEDe2XjaVp4UM5herCAwyv7tboEES9
lFP+xxZamBmXp5uC+jcYSDmiuhdShSXELrweP1kcaiiUzDd1+4aO+mCNTWxhfOO2
SrHi/ttYEym7co9LbB99TeRXPyUUvLkXjpurkAKBbf0mnHWhVQqjqquS9eOjfPZa
CxvgQf7IGk/ArVCTryXCVAmbdFxr+BGXf/eL6LvL7m3UvKmQJuuOuqfZXPweXYE0
6uy6aiUa/UmAFlHgfATW52RpHApISGfpPp5hi0w4rPMeVY9DZ0UALYDsELrsszCb
aHgaIT4rFN9jv532mNRctFynIWa+df+6t+DTfH/Y1/0uQcvgkgUQpnl6FtDz/4fH
eQGHM0BIGpdnGnoS7El3klmIzfK+Bv8/dDid1/8mjcP74Rvg/JQj4cb9rsCWFJJO
vdzLqFm+lJ5nramv/tIMsgV11n8l9Qf9safi7gF8eXpWFnETxgN/gjU9nXYTqiKN
IflWDQrl7BP86EElwd2hLj9ucAne9mypmucbG1cFs6QUyW55IhbH+ukJ4TUDsfvg
jNDJPDovkN7x8p/aA35nPrEOMS9MvSv6T02LwRU1ok/dEA2yx3s=
=eV3V
-----END PGP SIGNATURE-----
commit

464a159354a2ab4d0008584af1f18e37b453d8be

parent

6911337ffd467a2b1d977bccd130010b66e5bb2a

A admin/admin.go

@@ -0,0 +1,71 @@

+package admin + +import ( + "json" + "net/http" + "nilfm.cc/git/felt/models" + "nilfm.cc/git/quartzgun/auth" + "nilfm.cc/git/quartzgun/cookie" + "nilfm.cc/git/quartzgun/indentalUserDB" + . "nilfm.cc/git/quartzgun/middleware" + "nilfm.cc/git/quartzgun/renderer" + "nilfm.cc/git/quartzgun/router" + "strings" +) + +func getUserFromToken(req *http.Request) string { + authHeader := req.Header.Get("Authorization") + if strings.HasPrefix(authHeader, "Bearer ") { + authToken := strings.Split(authHeader, "Bearer ")[1] + data, err := base64.StdEncoding.DecodeString(token) + if err == nil { + parts := strings.Split(string(data), "\n") + if len(parts) == 2 { + return parts[0] + } + } + } + return nil +} + +func apiGetTableData(next http.Handler, udb auth.UserStore) http.Handler { + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + + // get username from + + rawTableData, err := udb.GetData(user, "tables") + if err != nil { + // handle error - return 404 or 500? + } + + // split rawTableData - tableName,passCode;tableName,passCode; + tables := strings.Split(rawTableData, ";") + self := make([]models.TableKey) + for _, t := range tables { + parts := strings.Split(t, ",") + if len(parts) == 2 { + self = append(self, models.TableKey{ + Name: parts[0], + Passcode: parts[1], + }) + } + } + + *req = *req.WithContext(context.WithValue(req.Context(), "tableData", self)) + next.serveHTTP(w, req) + } + + return handlerFunc +} + +func CreateAdminInterface(udb auth.UserStore) http.Handler { + // create quartzgun router + rtr := &router.Router{} + + rtr.Post("api/auth", Provision(udb, 84)) + + // initialize routes with admin interface + rtr.Get(`api/table/?P<Slug>\S+)`, Validate(apiGetTableData(renderer.JSON("tableData"), udb))) + + return router.ServeHTTP +}
A cmd/cmd.go

@@ -0,0 +1,37 @@

+package cmd + +import ( + "fmt" + "nilfm.cc/git/quartzgun/auth" + "strings" +) + +func ProcessCmd(args []string, userStore auth.UserStore, cfg *Config) bool { + if len(args) == 1 { + return false + } + switch args[1] { + case "adduser": + if len(args) < 4 { + return help() + } + userStore.AddUser(args[2], args[3]) + case "rmuser": + if len(args) < 3 { + return help() + } + userStore.DeleteUser(args[2]) + case "passwd": + if len(args) < 5 { + return help() + } + userStore.ChangePassword(args[2], args[3], args[4]) + default: + help() + } + return true +} + +func help() bool { + return true +}
M gametable/server.gogametable/server.go

@@ -8,8 +8,8 @@ "io/ioutil"

"log" "net/http" "nhooyr.io/websocket" - "nilfm.cc/git/felt/mongodb" "nilfm.cc/git/felt/models" + "nilfm.cc/git/felt/mongodb" "nilfm.cc/git/quartzgun/cookie" "sync" "time"

@@ -38,7 +38,7 @@ subscribers: make(map[*Subscriber]models.TableKey),

publishLimiter: rate.NewLimiter(rate.Every(time.Millisecond*100), 8), dbAdapter: adapter, } - srvr.serveMux.Handle("/", http.FileServer(http.Dir("./static"))) + srvr.serveMux.Handle("/table/", http.FileServer(http.Dir("./static"))) srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler) srvr.serveMux.HandleFunc("/publish", srvr.publishHandler)
M main.gomain.go

@@ -5,8 +5,8 @@ "context"

"log" "net" "net/http" - "nilfm.cc/git/felt/mongodb" "nilfm.cc/git/felt/gametable" + "nilfm.cc/git/felt/mongodb" "os" "os/signal" "time"

@@ -24,14 +24,12 @@ l, err := net.Listen("tcp", os.Args[1])

if err != nil { return err } - - dbEngine := &mongodb.DbEngine{} - err = dbEngine.Init(os.Args[2]) + + dbEngine := &mongodb.DbEngine{} + err = dbEngine.Init(os.Args[2]) if err != nil { - return err + return err } - - gt := gametable.New(dbEngine) s := &http.Server{
M models/models.gomodels/models.go

@@ -32,12 +32,12 @@ MapImageUrl string

DiceRolls []DiceRoll Tokens []Token AvailableTokens []Token - AuxMessage string + AuxMessage string } type TableMessage struct { - Roll DiceRoll - Token Token - MapImg string - AuxMsg string -}+ Roll DiceRoll + Token Token + MapImg string + AuxMsg string +}
M mongodb/adapter.gomongodb/adapter.go

@@ -2,13 +2,13 @@ package mongodb

import ( "context" + "errors" + "fmt" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "nilfm.cc/git/felt/models" "time" - "errors" - "fmt" ) const errNoCollection string = "collection not found: felt.%s"

@@ -29,7 +29,7 @@ GetDiceRolls(table models.TableKey) ([]models.DiceRoll, error)

SetMapImageUrl(table models.TableKey, url string) error GetMapImageUrl(table models.TableKey) (string, error) - + SetAuxMessage(table models.TableKey, message string) error GetAuxMessage(table models.TableKey) (string, error)

@@ -297,7 +297,7 @@ {"name", table.Name},

{"passcode", table.Passcode}, }, bson.D{ - {"$pull", bson.D{{tokenArrKey, bson.E{"_id", tokenId}}}}, + {"$pull", bson.D{{tokenArrKey, bson.D{{"_id", tokenId}}}}}, }, ).Decode(&result) return err
A notes.txt

@@ -0,0 +1,30 @@

+admin routes: + - Get /login + + - Post api/auth + + - Get /dash { + dashboard. show list of tables, new table + } + + - Get /new { + new table interface + } + - Post /new { + create new table + } + + - Get /table/<name> { + edit given table - standard table view plus admin features + - manage availableTokens via /storage/ routes + - manage map bg + } + - Post/Put /storage/<table>/<type>/<name> { + upload token or map bg + } + - Delete /storage/<table>/<type>/<name> { + delete token + } + - Get /storage/ { + static storage tree + }
A static/index.html

@@ -0,0 +1,50 @@

+<!DOCTYPE html> +<html lang="en-US"> + <head> + <meta charset="UTF-8" /> + <title>Felt</title> + <meta name="viewport" content="width=device-width" /> + <link href="/style.css" rel="stylesheet" /> + </head> + <body> + <nav> + <input id="name_entry"> + <button id="goto_table">Change Table</button> + <button id="admin_login">Admin Login</button> + </nav> + <div id="dynamic_modal"></div> + <div id="dice_log"></div> + <select name="num_dice"> + <option>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> + <option>6</option> + <option>7</option> + <option>8</option> + <option>9</option> + <option>10</option> + <option>11</option> + <option>12</option> + <option>13</option> + <option>14</option> + <option>15</option> + <option>16</option> + <option>17</option> + <option>18</option> + <option>19</option> + <option>20</option> + </select> + <label for="dice_faces">d</label> + <select id="dice_faces"> + <option>4</option> + <option selected>6</option> + <option>8</option> + <option>10</option> + <option>12</option> + <option>20</option> + </select> + <input id="dice_note"><button id="dice_submit">Roll</button> + <div id="map"></div> +</body>
A static/index.js

@@ -0,0 +1,76 @@

+;(() => { + // expectingMessage is set to true + // if the user has just submitted a message + // and so we should scroll the next message into view when received. + let expectingMessage = false + function dial() { + const conn = new WebSocket(`ws://${location.host}/subscribe`) + + conn.addEventListener("close", ev => { + appendLog(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`, true) + if (ev.code !== 1001) { + appendLog("Reconnecting in 1s", true) + setTimeout(dial, 1000) + } + }) + conn.addEventListener("open", ev => { + console.info("websocket connected") + }) + + // This is where we handle messages received. + conn.addEventListener("message", ev => { + if (typeof ev.data !== "string") { + console.error("unexpected message type", typeof ev.data) + return + } + const p = appendLog(ev.data) + if (expectingMessage) { + p.scrollIntoView() + expectingMessage = false + } + }) + } + dial() + + const messageLog = document.getElementById("message-log") + const publishForm = document.getElementById("publish-form") + const messageInput = document.getElementById("message-input") + + // appendLog appends the passed text to messageLog. + function appendLog(text, error) { + const p = document.createElement("p") + // Adding a timestamp to each message makes the log easier to read. + p.innerText = `${new Date().toLocaleTimeString()}: ${text}` + if (error) { + p.style.color = "red" + p.style.fontStyle = "bold" + } + messageLog.append(p) + return p + } + appendLog("Submit a message to get started!") + + // onsubmit publishes the message from the user when the form is submitted. + publishForm.onsubmit = async ev => { + ev.preventDefault() + + const msg = messageInput.value + if (msg === "") { + return + } + messageInput.value = "" + + expectingMessage = true + try { + const resp = await fetch("/publish", { + method: "POST", + body: msg, + }) + if (resp.status !== 202) { + throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText}`) + } + } catch (err) { + appendLog(`Publish failed: ${err.message}`, true) + } + } +})()