diff --git a/admin/admin.go b/admin/admin.go new file mode 100644 index 0000000..79ddc5a --- /dev/null +++ b/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\S+)`, Validate(apiGetTableData(renderer.JSON("tableData"), udb))) + + return router.ServeHTTP +} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..79f024b --- /dev/null +++ b/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 +} diff --git a/gametable/server.go b/gametable/server.go index 7c15d22..7b898b8 100644 --- a/gametable/server.go +++ b/gametable/server.go @@ -8,8 +8,8 @@ import ( "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 @@ func New(adapter mongodb.DbAdapter) *GameTableServer { 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) diff --git a/main.go b/main.go index a577d14..4963adf 100644 --- a/main.go +++ b/main.go @@ -5,8 +5,8 @@ import ( "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 @@ func run() error { if err != nil { return err } - - dbEngine := &mongodb.DbEngine{} - err = dbEngine.Init(os.Args[2]) - if err != nil { - return err - } - + dbEngine := &mongodb.DbEngine{} + err = dbEngine.Init(os.Args[2]) + if err != nil { + return err + } gt := gametable.New(dbEngine) s := &http.Server{ diff --git a/models/models.go b/models/models.go index f9a7c0d..4dcc435 100644 --- a/models/models.go +++ b/models/models.go @@ -32,12 +32,12 @@ type Table struct { DiceRolls []DiceRoll Tokens []Token AvailableTokens []Token - AuxMessage string + AuxMessage string } type TableMessage struct { - Roll DiceRoll - Token Token - MapImg string - AuxMsg string -} \ No newline at end of file + Roll DiceRoll + Token Token + MapImg string + AuxMsg string +} diff --git a/mongodb/adapter.go b/mongodb/adapter.go index 28382d2..8d8399c 100644 --- a/mongodb/adapter.go +++ b/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 @@ type DbAdapter interface { 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 @@ func (self *DbEngine) RemoveToken(table models.TableKey, tokenId string, active {"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 diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..1bc7365 --- /dev/null +++ b/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/ { + edit given table - standard table view plus admin features + - manage availableTokens via /storage/ routes + - manage map bg + } + - Post/Put /storage/// { + upload token or map bg + } + - Delete /storage/
// { + delete token + } + - Get /storage/ { + static storage tree + } \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..78c696e --- /dev/null +++ b/static/index.html @@ -0,0 +1,50 @@ + + + + + Felt + + + + + +
+
+ + + + +
+ \ No newline at end of file diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..15e2e4a --- /dev/null +++ b/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) + } + } +})() \ No newline at end of file