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-----
9 files changed,
281 insertions(+),
19 deletions(-)
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.go
→
gametable/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.go
→
main.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.go
→
models/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.go
→
mongodb/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) + } + } +})()