starting admin middleware
This commit is contained in:
parent
6911337ffd
commit
464a159354
9 changed files with 282 additions and 20 deletions
71
admin/admin.go
Normal file
71
admin/admin.go
Normal file
|
@ -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
|
||||||
|
}
|
37
cmd/cmd.go
Normal file
37
cmd/cmd.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
"nilfm.cc/git/felt/mongodb"
|
|
||||||
"nilfm.cc/git/felt/models"
|
"nilfm.cc/git/felt/models"
|
||||||
|
"nilfm.cc/git/felt/mongodb"
|
||||||
"nilfm.cc/git/quartzgun/cookie"
|
"nilfm.cc/git/quartzgun/cookie"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -38,7 +38,7 @@ func New(adapter mongodb.DbAdapter) *GameTableServer {
|
||||||
publishLimiter: rate.NewLimiter(rate.Every(time.Millisecond*100), 8),
|
publishLimiter: rate.NewLimiter(rate.Every(time.Millisecond*100), 8),
|
||||||
dbAdapter: adapter,
|
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("/subscribe", srvr.subscribeHandler)
|
||||||
srvr.serveMux.HandleFunc("/publish", srvr.publishHandler)
|
srvr.serveMux.HandleFunc("/publish", srvr.publishHandler)
|
||||||
|
|
||||||
|
|
14
main.go
14
main.go
|
@ -5,8 +5,8 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"nilfm.cc/git/felt/mongodb"
|
|
||||||
"nilfm.cc/git/felt/gametable"
|
"nilfm.cc/git/felt/gametable"
|
||||||
|
"nilfm.cc/git/felt/mongodb"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"time"
|
"time"
|
||||||
|
@ -24,14 +24,12 @@ func run() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
gt := gametable.New(dbEngine)
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
|
|
|
@ -32,12 +32,12 @@ type Table struct {
|
||||||
DiceRolls []DiceRoll
|
DiceRolls []DiceRoll
|
||||||
Tokens []Token
|
Tokens []Token
|
||||||
AvailableTokens []Token
|
AvailableTokens []Token
|
||||||
AuxMessage string
|
AuxMessage string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TableMessage struct {
|
type TableMessage struct {
|
||||||
Roll DiceRoll
|
Roll DiceRoll
|
||||||
Token Token
|
Token Token
|
||||||
MapImg string
|
MapImg string
|
||||||
AuxMsg string
|
AuxMsg string
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@ package mongodb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
"nilfm.cc/git/felt/models"
|
"nilfm.cc/git/felt/models"
|
||||||
"time"
|
"time"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const errNoCollection string = "collection not found: felt.%s"
|
const errNoCollection string = "collection not found: felt.%s"
|
||||||
|
@ -29,7 +29,7 @@ type DbAdapter interface {
|
||||||
|
|
||||||
SetMapImageUrl(table models.TableKey, url string) error
|
SetMapImageUrl(table models.TableKey, url string) error
|
||||||
GetMapImageUrl(table models.TableKey) (string, error)
|
GetMapImageUrl(table models.TableKey) (string, error)
|
||||||
|
|
||||||
SetAuxMessage(table models.TableKey, message string) error
|
SetAuxMessage(table models.TableKey, message string) error
|
||||||
GetAuxMessage(table models.TableKey) (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},
|
{"passcode", table.Passcode},
|
||||||
},
|
},
|
||||||
bson.D{
|
bson.D{
|
||||||
{"$pull", bson.D{{tokenArrKey, bson.E{"_id", tokenId}}}},
|
{"$pull", bson.D{{tokenArrKey, bson.D{{"_id", tokenId}}}}},
|
||||||
},
|
},
|
||||||
).Decode(&result)
|
).Decode(&result)
|
||||||
return err
|
return err
|
||||||
|
|
30
notes.txt
Normal file
30
notes.txt
Normal file
|
@ -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
|
||||||
|
}
|
50
static/index.html
Normal file
50
static/index.html
Normal file
|
@ -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>
|
76
static/index.js
Normal file
76
static/index.js
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
Loading…
Reference in a new issue