delete assets when destroying table, clear dice message on roll, clear map on socket disconnect; start registration controller
This commit is contained in:
parent
e7081a5320
commit
952f80dbc2
15 changed files with 151 additions and 35 deletions
|
@ -110,7 +110,7 @@ func apiCreateTable(next http.Handler, udb auth.UserStore, dbAdapter mongodb.DbA
|
||||||
return http.HandlerFunc(handlerFunc)
|
return http.HandlerFunc(handlerFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiDestroyTable(next http.Handler, udb auth.UserStore, dbAdapter mongodb.DbAdapter) http.Handler {
|
func apiDestroyTable(next http.Handler, udb auth.UserStore, dbAdapter mongodb.DbAdapter, uploads string) http.Handler {
|
||||||
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
|
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
|
||||||
// check table actually belongs to this user
|
// check table actually belongs to this user
|
||||||
user := util.GetUserFromToken(req)
|
user := util.GetUserFromToken(req)
|
||||||
|
@ -139,6 +139,7 @@ func apiDestroyTable(next http.Handler, udb auth.UserStore, dbAdapter mongodb.Db
|
||||||
}
|
}
|
||||||
|
|
||||||
if destroy {
|
if destroy {
|
||||||
|
os.RemoveAll(filepath.Join(uploads, table.Name))
|
||||||
newTables := append(tables[:i], tables[i+1:]...)
|
newTables := append(tables[:i], tables[i+1:]...)
|
||||||
util.SetTablesForUser(user, newTables, udb)
|
util.SetTablesForUser(user, newTables, udb)
|
||||||
w.WriteHeader(204)
|
w.WriteHeader(204)
|
||||||
|
@ -338,7 +339,7 @@ func apiDeleteImage(next http.Handler, uploads string, uploadType string, udb au
|
||||||
|
|
||||||
func CreateAdminInterface(udb auth.UserStore, dbAdapter mongodb.DbAdapter, uploads string, uploadMaxMB int) http.Handler {
|
func CreateAdminInterface(udb auth.UserStore, dbAdapter mongodb.DbAdapter, uploads string, uploadMaxMB int) http.Handler {
|
||||||
// create quartzgun router
|
// create quartzgun router
|
||||||
rtr := &router.Router{Fallback: *template.Must(template.ParseFiles("static/error.html"))}
|
rtr := &router.Router{Fallback: *template.Must(template.ParseFiles("templates/error.html"))}
|
||||||
|
|
||||||
scopes := map[string]string{}
|
scopes := map[string]string{}
|
||||||
|
|
||||||
|
@ -348,7 +349,7 @@ func CreateAdminInterface(udb auth.UserStore, dbAdapter mongodb.DbAdapter, uploa
|
||||||
rtr.Get("/api/table/", Validate(apiGetTableList(renderer.JSON("tableList"), udb), udb, scopes))
|
rtr.Get("/api/table/", Validate(apiGetTableList(renderer.JSON("tableList"), udb), udb, scopes))
|
||||||
rtr.Get(`/api/table/(?P<Slug>\S+)`, Validate(apiGetTableData(renderer.JSON("tableData"), udb, dbAdapter), udb, scopes))
|
rtr.Get(`/api/table/(?P<Slug>\S+)`, Validate(apiGetTableData(renderer.JSON("tableData"), udb, dbAdapter), udb, scopes))
|
||||||
rtr.Post("/api/table/", Validate(apiCreateTable(renderer.JSON("result"), udb, dbAdapter), udb, scopes))
|
rtr.Post("/api/table/", Validate(apiCreateTable(renderer.JSON("result"), udb, dbAdapter), udb, scopes))
|
||||||
rtr.Delete(`/api/table/(?P<Slug>\S+)`, Validate(apiDestroyTable(renderer.JSON("result"), udb, dbAdapter), udb, scopes))
|
rtr.Delete(`/api/table/(?P<Slug>\S+)`, Validate(apiDestroyTable(renderer.JSON("result"), udb, dbAdapter, uploads), udb, scopes))
|
||||||
|
|
||||||
// asset management
|
// asset management
|
||||||
rtr.Post(`/api/upload/(?P<Slug>\S+)/map/`, Validate(apiUploadImg(renderer.JSON("location"), dbAdapter, uploads, "map", uploadMaxMB), udb, scopes))
|
rtr.Post(`/api/upload/(?P<Slug>\S+)/map/`, Validate(apiUploadImg(renderer.JSON("location"), dbAdapter, uploads, "map", uploadMaxMB), udb, scopes))
|
||||||
|
@ -357,7 +358,6 @@ func CreateAdminInterface(udb auth.UserStore, dbAdapter mongodb.DbAdapter, uploa
|
||||||
rtr.Delete(`/api/upload/(?P<table>\S+)/token/(?P<file>\S+)`, Validate(apiDeleteImage(renderer.JSON("deleted"), uploads, "token", udb, dbAdapter), udb, scopes))
|
rtr.Delete(`/api/upload/(?P<table>\S+)/token/(?P<file>\S+)`, Validate(apiDeleteImage(renderer.JSON("deleted"), uploads, "token", udb, dbAdapter), udb, scopes))
|
||||||
rtr.Post(`/api/upload/(?P<Slug>\S+)/token/`, Validate(apiUploadImg(renderer.JSON("location"), dbAdapter, uploads, "token", uploadMaxMB), udb, scopes))
|
rtr.Post(`/api/upload/(?P<Slug>\S+)/token/`, Validate(apiUploadImg(renderer.JSON("location"), dbAdapter, uploads, "token", uploadMaxMB), udb, scopes))
|
||||||
rtr.Get(`/api/upload/(?P<Slug>\S+)/token/`, Validate(apiListImages(renderer.JSON("files"), uploads, "token", udb, dbAdapter), udb, scopes))
|
rtr.Get(`/api/upload/(?P<Slug>\S+)/token/`, Validate(apiListImages(renderer.JSON("files"), uploads, "token", udb, dbAdapter), udb, scopes))
|
||||||
// DELETE /api/upload/<table>/token/<token>
|
|
||||||
|
|
||||||
return http.HandlerFunc(rtr.ServeHTTP)
|
return http.HandlerFunc(rtr.ServeHTTP)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ type Config struct {
|
||||||
Uploads string
|
Uploads string
|
||||||
UploadMaxMB int
|
UploadMaxMB int
|
||||||
MongoURI string
|
MongoURI string
|
||||||
|
RegistrationSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConfigLocation() string {
|
func GetConfigLocation() string {
|
||||||
|
@ -81,6 +82,10 @@ func (self *Config) RunWizard() {
|
||||||
fmt.Printf("Max file upload size (MB)? ")
|
fmt.Printf("Max file upload size (MB)? ")
|
||||||
self.UploadMaxMB = ensureNumberOption(&inputBuf)
|
self.UploadMaxMB = ensureNumberOption(&inputBuf)
|
||||||
|
|
||||||
|
fmt.Printf("Encryption secret for admin invite codes? ")
|
||||||
|
ensureNonEmptyOption(&inputBuf)
|
||||||
|
self.RegistrationSecret = inputBuf
|
||||||
|
|
||||||
fmt.Printf("Configuration complete!\n")
|
fmt.Printf("Configuration complete!\n")
|
||||||
self.Write()
|
self.Write()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"hacklab.nilfm.cc/felt/admin"
|
"hacklab.nilfm.cc/felt/admin"
|
||||||
"hacklab.nilfm.cc/felt/models"
|
"hacklab.nilfm.cc/felt/models"
|
||||||
"hacklab.nilfm.cc/felt/mongodb"
|
"hacklab.nilfm.cc/felt/mongodb"
|
||||||
|
"hacklab.nilfm.cc/felt/register"
|
||||||
"hacklab.nilfm.cc/quartzgun/auth"
|
"hacklab.nilfm.cc/quartzgun/auth"
|
||||||
"hacklab.nilfm.cc/quartzgun/renderer"
|
"hacklab.nilfm.cc/quartzgun/renderer"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -37,7 +38,7 @@ type GameTableServer struct {
|
||||||
udb auth.UserStore
|
udb auth.UserStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(adapter mongodb.DbAdapter, udb auth.UserStore, uploads string, uploadMaxMB int) *GameTableServer {
|
func New(adapter mongodb.DbAdapter, udb auth.UserStore, uploads string, uploadMaxMB int, registrationSecret string) *GameTableServer {
|
||||||
srvr := &GameTableServer{
|
srvr := &GameTableServer{
|
||||||
subscribeMessageBuffer: 16,
|
subscribeMessageBuffer: 16,
|
||||||
logf: log.Printf,
|
logf: log.Printf,
|
||||||
|
@ -49,6 +50,7 @@ func New(adapter mongodb.DbAdapter, udb auth.UserStore, uploads string, uploadMa
|
||||||
srvr.serveMux.Handle("/table/", http.StripPrefix("/table/", renderer.Subtree("./static")))
|
srvr.serveMux.Handle("/table/", http.StripPrefix("/table/", renderer.Subtree("./static")))
|
||||||
srvr.serveMux.Handle("/uploads/", http.StripPrefix("/uploads/", renderer.Subtree(uploads)))
|
srvr.serveMux.Handle("/uploads/", http.StripPrefix("/uploads/", renderer.Subtree(uploads)))
|
||||||
srvr.serveMux.Handle("/admin/", http.StripPrefix("/admin", admin.CreateAdminInterface(udb, adapter, uploads, uploadMaxMB)))
|
srvr.serveMux.Handle("/admin/", http.StripPrefix("/admin", admin.CreateAdminInterface(udb, adapter, uploads, uploadMaxMB)))
|
||||||
|
srvr.serveMux.Handle("/register/", http.StripPrefix("/register", register.CreateRegistrationInterface(udb, registrationSecret)))
|
||||||
srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler)
|
srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler)
|
||||||
srvr.serveMux.HandleFunc("/publish", srvr.publishHandler)
|
srvr.serveMux.HandleFunc("/publish", srvr.publishHandler)
|
||||||
|
|
||||||
|
|
2
main.go
2
main.go
|
@ -48,7 +48,7 @@ func run() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
gt := gametable.New(dbEngine, udb, cfg.Uploads, cfg.UploadMaxMB)
|
gt := gametable.New(dbEngine, udb, cfg.Uploads, cfg.UploadMaxMB, cfg.RegistrationSecret)
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
Handler: gt,
|
Handler: gt,
|
||||||
ReadTimeout: time.Second * 10,
|
ReadTimeout: time.Second * 10,
|
||||||
|
|
91
register/register.go
Normal file
91
register/register.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package register
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hacklab.nilfm.cc/quartzgun/auth"
|
||||||
|
"hacklab.nilfm.cc/quartzgun/renderer"
|
||||||
|
"hacklab.nilfm.cc/quartzgun/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bytes = []byte{99, 207, 33, 57, 28, 01, 50, 76, 01}
|
||||||
|
|
||||||
|
type SymmetricCrypto interface {
|
||||||
|
Encode(b []byte) string
|
||||||
|
Decode(s string) []byte
|
||||||
|
Encrypt(text string) (string, error)
|
||||||
|
Decrypt(text string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymmetricCrypt struct {
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SymmetricCrypt) Encode(b []byte) string {
|
||||||
|
return base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SymmetricCrypt) Decode(s string) []byte {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SymmetricCrypt) Encrypt(text string) (string, error) {
|
||||||
|
block, err := aes.NewCipher([]byte(self.Secret))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
plainText := []byte(text)
|
||||||
|
cfb := cipher.NewCFBEncrypter(block, bytes)
|
||||||
|
cipherText := make([]byte, len(plainText))
|
||||||
|
cfb.XORKeyStream(cipherText, plainText)
|
||||||
|
return self.Encode(cipherText), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SymmetricCrypt) Decrypt(text string) (string, error) {
|
||||||
|
block, err := aes.NewCipher([]byte(self.Secret))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cipherText := self.Decode(text)
|
||||||
|
cfb := cipher.NewCFBDecrypter(block, bytes)
|
||||||
|
plainText := make([]byte, len(cipherText))
|
||||||
|
cfb.XORKeyStream(plainText, cipherText)
|
||||||
|
return string(plainText), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCrypto(next http.Handler, crypto SymmetricCrypto) http.Handler {
|
||||||
|
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
*req = *req.WithContext(context.WithValue(req.Context(), "crypto", crypto))
|
||||||
|
next.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(handlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithUserStore(next http.Handler, udb auth.UserStore) http.Handler {
|
||||||
|
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
*req = *req.WithContext(context.WithValue(req.Context(), "udb", udb))
|
||||||
|
next.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(handlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRegistrationInterface(udb auth.UserStore, secret string) http.Handler {
|
||||||
|
rtr := &router.Router{Fallback: *template.Must(template.ParseFiles("templates/error.html"))}
|
||||||
|
crypto := &SymmetricCrypt{Secret: secret}
|
||||||
|
|
||||||
|
rtr.Get(`/(?P<cipher>.*)`, WithCrypto(renderer.Template("templates/register.html"), crypto))
|
||||||
|
rtr.Post(`/(?P<cipher>.*)`, WithUserStore(WithCrypto(renderer.Template("templates/registered.html"), crypto), udb))
|
||||||
|
|
||||||
|
return http.HandlerFunc(rtr.ServeHTTP)
|
||||||
|
}
|
|
@ -180,9 +180,11 @@ function drawTokenOrigin() {
|
||||||
function reinitializeSpritePreview() {
|
function reinitializeSpritePreview() {
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].value;
|
img.src = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].value;
|
||||||
|
|
||||||
const tokenNameParts = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].text.split(".");
|
const tokenNameParts = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].text.split(".");
|
||||||
tokenNameParts.pop();
|
tokenNameParts.pop();
|
||||||
tokenName.value = tokenNameParts.join(".");
|
tokenName.value = tokenNameParts.join(".");
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
const w = img.naturalWidth;
|
const w = img.naturalWidth;
|
||||||
const h = img.naturalHeight;
|
const h = img.naturalHeight;
|
||||||
|
@ -191,6 +193,7 @@ function reinitializeSpritePreview() {
|
||||||
tokenHeight.value = h;
|
tokenHeight.value = h;
|
||||||
scaleSpritePreview();
|
scaleSpritePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
previewZone.innerHTML = "";
|
previewZone.innerHTML = "";
|
||||||
previewZone.appendChild(img);
|
previewZone.appendChild(img);
|
||||||
}
|
}
|
||||||
|
@ -425,12 +428,20 @@ async function doLogin() {
|
||||||
adminWrapper.style.display="inline";
|
adminWrapper.style.display="inline";
|
||||||
adminZone.style.display = "block";
|
adminZone.style.display = "block";
|
||||||
closeErr();
|
closeErr();
|
||||||
|
replaceAdminModal();
|
||||||
} else {
|
} else {
|
||||||
setErr("Incorrect credentials");
|
setErr("Incorrect credentials");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceAdminModal() {
|
||||||
|
const adminModal = document.getElementById("admin_modal");
|
||||||
|
if (adminModal) {
|
||||||
|
adminModal.innerHTML = "<a href='./'>Logout</a>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getAdminToken(user, pass) {
|
async function getAdminToken(user, pass) {
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set('Authorization', 'Basic ' + btoa(user + ":" + pass));
|
headers.set('Authorization', 'Basic ' + btoa(user + ":" + pass));
|
||||||
|
|
|
@ -30,5 +30,6 @@ function rollDice() {
|
||||||
note: note.value,
|
note: note.value,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}});
|
}});
|
||||||
|
note.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<section id="user_section">
|
<section id="user_section">
|
||||||
<details class="ui_win"><summary>identity</summary>
|
<details class="ui_win" open><summary>identity</summary>
|
||||||
<label for="name_entry">username</label>
|
<label for="name_entry">username</label>
|
||||||
<input id="name_entry" onblur="saveName()">
|
<input id="name_entry" onblur="saveName()">
|
||||||
</details><br/>
|
</details><br/>
|
||||||
|
|
|
@ -7,7 +7,7 @@ function initializeMap(mapImgUrl) {
|
||||||
let init = false;
|
let init = false;
|
||||||
if (!map) {
|
if (!map) {
|
||||||
init = true;
|
init = true;
|
||||||
map = L.map('map', { minZoom: 0, maxZoom: 4, crs: L.CRS.Simple });
|
map = L.map('map', { minZoom: 0, maxZoom: 4, crs: L.CRS.Simple, attributionControl: true, zoomControl: false });
|
||||||
map.on("zoomend", ()=>{resizeMarkers();scaleSpritePreview();});
|
map.on("zoomend", ()=>{resizeMarkers();scaleSpritePreview();});
|
||||||
}
|
}
|
||||||
if (mapImg) {
|
if (mapImg) {
|
||||||
|
@ -19,8 +19,6 @@ function initializeMap(mapImgUrl) {
|
||||||
if (init) {
|
if (init) {
|
||||||
map.setView([0,0], 2);
|
map.setView([0,0], 2);
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this works but assumes the map is square (reasonable limitation I think)
|
// this works but assumes the map is square (reasonable limitation I think)
|
||||||
|
|
|
@ -89,7 +89,7 @@ function makeUpToDate(table) {
|
||||||
if (table.diceRolls) {
|
if (table.diceRolls) {
|
||||||
logDice(table.diceRolls);
|
logDice(table.diceRolls);
|
||||||
} else if (table.diceRoll) {
|
} else if (table.diceRoll) {
|
||||||
logDice([table.diceRoll]);
|
logDice(table.diceRoll);
|
||||||
}
|
}
|
||||||
if (table.tokens) {
|
if (table.tokens) {
|
||||||
updateTokens(table.tokens);
|
updateTokens(table.tokens);
|
||||||
|
@ -139,6 +139,8 @@ function dial() {
|
||||||
tokens[0].m.removeFrom(map);
|
tokens[0].m.removeFrom(map);
|
||||||
tokens.shift();
|
tokens.shift();
|
||||||
}
|
}
|
||||||
|
mapImg.removeFrom(map);
|
||||||
|
mapImg = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
conn.addEventListener("open", e => {
|
conn.addEventListener("open", e => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
:root {
|
:root {
|
||||||
--bg_color: rgba(0,0,0,0.7);
|
--bg_color: #000000cc;
|
||||||
--fg_color: #ccc;
|
--fg_color: #ccc;
|
||||||
--main_color: #1f9b92;
|
--main_color: #1f9b92;
|
||||||
--sub_color: #002b36;
|
--sub_color: #002b36;
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
const errDiv = document.getElementById("errDiv");
|
const errDiv = document.getElementById("errDiv");
|
||||||
const errWrapper = document.getElementById("errWrapper");
|
const errWrapper = document.getElementById("errWrapper");
|
||||||
|
|
||||||
|
const defaultTheme = [ "#000000cc", "#ccccccff", "#1f9b92ff", "#002b36ff" ];
|
||||||
|
|
||||||
|
const saveData = {
|
||||||
|
username: "",
|
||||||
|
theme: defaultTheme,
|
||||||
|
}
|
||||||
|
|
||||||
function setErr(x) {
|
function setErr(x) {
|
||||||
if (errDiv) {
|
if (errDiv) {
|
||||||
errDiv.innerHTML = x;
|
errDiv.innerHTML = x;
|
||||||
|
@ -16,23 +23,22 @@ function closeErr() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadStorage() {
|
||||||
|
saveData.username = localStorage.getItem("username");
|
||||||
|
saveData.theme = JSON.parse(localStorage.getItem("theme"));
|
||||||
|
|
||||||
|
const username = document.getElementById("name_entry");
|
||||||
|
if (username) {
|
||||||
|
username.value = saveData.username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function saveName() {
|
function saveName() {
|
||||||
console.log("saving username");
|
console.log("saving username");
|
||||||
const username = document.getElementById("name_entry");
|
const username = document.getElementById("name_entry");
|
||||||
if (username) {
|
if (username) {
|
||||||
document.cookie = "username=" + username.value;
|
saveData.username = username.value;
|
||||||
}
|
localStorage.setItem("username", saveData.username);
|
||||||
}
|
|
||||||
|
|
||||||
function loadName() {
|
|
||||||
const username = document.getElementById("name_entry");
|
|
||||||
if (username) {
|
|
||||||
const cookies = document.cookie.split(";")
|
|
||||||
cookies.forEach(c=>{
|
|
||||||
if (c.trim().startsWith("username=")) {
|
|
||||||
username.value = c.trim().split("=")[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,4 +53,4 @@ function setupDiceAutoScroll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDiceAutoScroll();
|
setupDiceAutoScroll();
|
||||||
loadName();
|
loadStorage();
|
0
templates/register.html
Normal file
0
templates/register.html
Normal file
0
templates/registered.html
Normal file
0
templates/registered.html
Normal file
Loading…
Reference in a new issue