autogenerate registration secret/IV, add error color to theme, upgrade leaflet, and add left-hand token preview
This commit is contained in:
parent
7cdcdf5528
commit
d82b22f621
18 changed files with 287 additions and 86 deletions
20
cmd/cmd.go
20
cmd/cmd.go
|
@ -25,25 +25,35 @@ func ProcessCmd(args []string, userStore auth.UserStore, crypto register.Symmetr
|
||||||
}
|
}
|
||||||
case "adduser":
|
case "adduser":
|
||||||
if len(args) < 4 {
|
if len(args) < 4 {
|
||||||
return help()
|
return help(args[0])
|
||||||
}
|
}
|
||||||
userStore.AddUser(args[2], args[3])
|
userStore.AddUser(args[2], args[3])
|
||||||
case "rmuser":
|
case "rmuser":
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
return help()
|
return help(args[0])
|
||||||
}
|
}
|
||||||
userStore.DeleteUser(args[2])
|
userStore.DeleteUser(args[2])
|
||||||
case "passwd":
|
case "passwd":
|
||||||
if len(args) < 5 {
|
if len(args) < 5 {
|
||||||
return help()
|
return help(args[0])
|
||||||
}
|
}
|
||||||
userStore.ChangePassword(args[2], args[3], args[4])
|
userStore.ChangePassword(args[2], args[3], args[4])
|
||||||
default:
|
default:
|
||||||
help()
|
return help(args[0])
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func help() bool {
|
func help(prog string) bool {
|
||||||
|
fmt.Println("usage: " + prog + " [invite | adduser NAME PW | rmuser NAME | passwd NAME OLDPW NEWPW]")
|
||||||
|
fmt.Println(" commands:")
|
||||||
|
fmt.Println(" invite: generates a token to be appended to the /register/ endpoint to create an admin account")
|
||||||
|
fmt.Println(" adduser: adds a new admin user to the user.db")
|
||||||
|
fmt.Println(" rmuser: removes named user from the user.db")
|
||||||
|
fmt.Println(" passwd: change a user's password")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println(" adduser, rmuser, and passwd should be run while the server is offline")
|
||||||
|
fmt.Println(" This is because external changes to the user.db will be overwritten if the server is running")
|
||||||
|
fmt.Println(" Codes generated via the invite command are good for 15 minutes")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"hacklab.nilfm.cc/quartzgun/cookie"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -15,6 +17,7 @@ type Config struct {
|
||||||
UploadMaxMB int
|
UploadMaxMB int
|
||||||
MongoURI string
|
MongoURI string
|
||||||
RegistrationSecret string
|
RegistrationSecret string
|
||||||
|
RegistrationSalt []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConfigLocation() string {
|
func GetConfigLocation() string {
|
||||||
|
@ -44,7 +47,15 @@ func ensureConfigLocationExists() {
|
||||||
|
|
||||||
func ReadConfig() *Config {
|
func ReadConfig() *Config {
|
||||||
ensureConfigLocationExists()
|
ensureConfigLocationExists()
|
||||||
return parseConfig(filepath.Join(GetConfigLocation(), "felt.conf"))
|
self := parseConfig(filepath.Join(GetConfigLocation(), "felt.conf"))
|
||||||
|
bytes, err := getSecrets(filepath.Join(GetConfigLocation(), "enclave"))
|
||||||
|
if err == nil {
|
||||||
|
self.RegistrationSalt = bytes[0:16]
|
||||||
|
self.RegistrationSecret = string(bytes[16:48])
|
||||||
|
} else {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Config) Write() error {
|
func (self *Config) Write() error {
|
||||||
|
@ -53,7 +64,7 @@ func (self *Config) Write() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Config) IsNull() bool {
|
func (self *Config) IsNull() bool {
|
||||||
return self.Port == 0 || self.UploadMaxMB == 0 || self.Uploads == ""
|
return self.Port == 0 || self.UploadMaxMB == 0 || self.Uploads == "" || len(self.RegistrationSalt) != 16 || len(self.RegistrationSecret) != 32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Config) RunWizard() {
|
func (self *Config) RunWizard() {
|
||||||
|
@ -82,24 +93,12 @@ 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? ")
|
setSecrets([]byte(cookie.GenToken(48)), filepath.Join(GetConfigLocation(), "enclave"))
|
||||||
ensure32BytePassphrase(&inputBuf)
|
|
||||||
self.RegistrationSecret = inputBuf
|
|
||||||
|
|
||||||
fmt.Printf("Configuration complete!\n")
|
fmt.Printf("Configuration complete! Registration secrets have been updated as well!\n")
|
||||||
self.Write()
|
self.Write()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensure32BytePassphrase(buffer *string) {
|
|
||||||
for {
|
|
||||||
fmt.Scanln(buffer)
|
|
||||||
if len([]byte(strings.TrimSpace(*buffer))) == 32 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Println("Please enter a 32-byte string")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureNonEmptyOption(buffer *string) {
|
func ensureNonEmptyOption(buffer *string) {
|
||||||
for {
|
for {
|
||||||
fmt.Scanln(buffer)
|
fmt.Scanln(buffer)
|
||||||
|
@ -161,6 +160,30 @@ func writeConfig(cfg *Config, configFile string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSecrets(secret []byte, secretFile string) error {
|
||||||
|
f, err := os.Create(secretFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
f.Write(secret)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSecrets(secretFile string) ([]byte, error) {
|
||||||
|
f, err := os.ReadFile(secretFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(f) != 48 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseConfig(configFile string) *Config {
|
func parseConfig(configFile string) *Config {
|
||||||
f, err := os.ReadFile(configFile)
|
f, err := os.ReadFile(configFile)
|
||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
|
|
|
@ -38,7 +38,7 @@ type GameTableServer struct {
|
||||||
udb auth.UserStore
|
udb auth.UserStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(adapter mongodb.DbAdapter, udb auth.UserStore, uploads string, uploadMaxMB int, registrationSecret string) *GameTableServer {
|
func New(adapter mongodb.DbAdapter, udb auth.UserStore, uploads string, uploadMaxMB int, crypto register.SymmetricCrypto) *GameTableServer {
|
||||||
srvr := &GameTableServer{
|
srvr := &GameTableServer{
|
||||||
subscribeMessageBuffer: 16,
|
subscribeMessageBuffer: 16,
|
||||||
logf: log.Printf,
|
logf: log.Printf,
|
||||||
|
@ -50,7 +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.Handle("/register/", http.StripPrefix("/register", register.CreateRegistrationInterface(udb, crypto)))
|
||||||
srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler)
|
srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler)
|
||||||
srvr.serveMux.HandleFunc("/publish", srvr.publishHandler)
|
srvr.serveMux.HandleFunc("/publish", srvr.publishHandler)
|
||||||
|
|
||||||
|
|
4
main.go
4
main.go
|
@ -34,7 +34,7 @@ func run() error {
|
||||||
udb := indentalUserDB.CreateIndentalUserDB(
|
udb := indentalUserDB.CreateIndentalUserDB(
|
||||||
filepath.Join(config.GetConfigLocation(), "user.db"))
|
filepath.Join(config.GetConfigLocation(), "user.db"))
|
||||||
|
|
||||||
crypto := ®ister.SymmetricCrypt{Secret: cfg.RegistrationSecret}
|
crypto := ®ister.SymmetricCrypt{Secret: cfg.RegistrationSecret, Iv: cfg.RegistrationSalt}
|
||||||
if cmd.ProcessCmd(os.Args, udb, crypto) {
|
if cmd.ProcessCmd(os.Args, udb, crypto) {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ func run() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
gt := gametable.New(dbEngine, udb, cfg.Uploads, cfg.UploadMaxMB, cfg.RegistrationSecret)
|
gt := gametable.New(dbEngine, udb, cfg.Uploads, cfg.UploadMaxMB, crypto)
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
Handler: gt,
|
Handler: gt,
|
||||||
ReadTimeout: time.Second * 10,
|
ReadTimeout: time.Second * 10,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -25,12 +24,10 @@ type SymmetricCrypto interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SymmetricCrypt struct {
|
type SymmetricCrypt struct {
|
||||||
|
Iv []byte
|
||||||
Secret string
|
Secret string
|
||||||
}
|
}
|
||||||
|
|
||||||
var iv []byte = []byte{107, 53, 46, 249, 52, 70, 36, 185,
|
|
||||||
168, 139, 144, 249, 242, 2, 125, 183}
|
|
||||||
|
|
||||||
func (self *SymmetricCrypt) IsValid(cipher string) bool {
|
func (self *SymmetricCrypt) IsValid(cipher string) bool {
|
||||||
stringTimestamp, err := self.Decrypt(cipher)
|
stringTimestamp, err := self.Decrypt(cipher)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -62,7 +59,7 @@ func (self *SymmetricCrypt) Encrypt(text string) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
plainText := []byte(text)
|
plainText := []byte(text)
|
||||||
cfb := cipher.NewCFBEncrypter(block, iv)
|
cfb := cipher.NewCFBEncrypter(block, self.Iv)
|
||||||
cipherText := make([]byte, len(plainText))
|
cipherText := make([]byte, len(plainText))
|
||||||
cfb.XORKeyStream(cipherText, plainText)
|
cfb.XORKeyStream(cipherText, plainText)
|
||||||
return self.Encode(cipherText), nil
|
return self.Encode(cipherText), nil
|
||||||
|
@ -74,7 +71,7 @@ func (self *SymmetricCrypt) Decrypt(text string) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
cipherText := self.Decode(text)
|
cipherText := self.Decode(text)
|
||||||
cfb := cipher.NewCFBDecrypter(block, iv)
|
cfb := cipher.NewCFBDecrypter(block, self.Iv)
|
||||||
plainText := make([]byte, len(cipherText))
|
plainText := make([]byte, len(cipherText))
|
||||||
cfb.XORKeyStream(plainText, cipherText)
|
cfb.XORKeyStream(plainText, cipherText)
|
||||||
return string(plainText), nil
|
return string(plainText), nil
|
||||||
|
@ -106,9 +103,8 @@ func WithUserStoreAndCrypto(next http.Handler, udb auth.UserStore, crypto Symmet
|
||||||
return http.HandlerFunc(handlerFunc)
|
return http.HandlerFunc(handlerFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateRegistrationInterface(udb auth.UserStore, secret string) http.Handler {
|
func CreateRegistrationInterface(udb auth.UserStore, crypto SymmetricCrypto) http.Handler {
|
||||||
rtr := &router.Router{Fallback: *template.Must(template.ParseFiles("templates/error.html"))}
|
rtr := &router.Router{Fallback: *template.Must(template.ParseFiles("templates/error.html"))}
|
||||||
crypto := &SymmetricCrypt{Secret: secret}
|
|
||||||
|
|
||||||
rtr.Get(`/(?P<cipher>\S+)`, WithCrypto(renderer.Template("templates/register.html"), crypto))
|
rtr.Get(`/(?P<cipher>\S+)`, WithCrypto(renderer.Template("templates/register.html"), crypto))
|
||||||
rtr.Post(`/(?P<cipher>\S+)`, WithUserStoreAndCrypto(renderer.Template("templates/registered.html"), udb, crypto))
|
rtr.Post(`/(?P<cipher>\S+)`, WithUserStoreAndCrypto(renderer.Template("templates/registered.html"), udb, crypto))
|
||||||
|
|
|
@ -155,6 +155,7 @@ function scaleSpritePreview(source) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function drawTokenOrigin() {
|
function drawTokenOrigin() {
|
||||||
if (tokenSpriteDropdown.selectedIndex >= 0) {
|
if (tokenSpriteDropdown.selectedIndex >= 0) {
|
||||||
const img = previewZone.children[0];
|
const img = previewZone.children[0];
|
||||||
|
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
|
@ -4,8 +4,9 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Felt</title>
|
<title>Felt</title>
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link href="./leaflet.css" rel="stylesheet" />
|
<link rel="shortcut icon" href="./favicon.png"/>
|
||||||
<link href="./style.css" rel="stylesheet" />
|
<link href="./leaflet.css?v=1.9.4" rel="stylesheet" />
|
||||||
|
<link href="./style.css?v=0.1.0" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
|
@ -17,19 +18,6 @@
|
||||||
<details class="ui_win" open><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 id="theme_accordion"><summary>theme</summary>
|
|
||||||
<form id="theme_cfg" onsubmit="return false">
|
|
||||||
<label>bg color<input type="color" id="bg_col_input"/></label><br/>
|
|
||||||
<label>bg opacity<input type="range" id="bg_col_opacity" min="0" max="255"/></label><br/>
|
|
||||||
<label>fg color<input type="color" id="fg_col_input"/></label><br/>
|
|
||||||
<label>fg opacity<input type="range" id="fg_col_opacity" min="0" max="255"/></label><br/>
|
|
||||||
<label>main color<input type="color" id="main_col_input"/></label><br/>
|
|
||||||
<label>main opacity<input type="range" id="main_col_opacity" min="0" max="255"/></label><br/>
|
|
||||||
<label>sub color<input type="color" id="sub_col_input"/></label><br/>
|
|
||||||
<label>sub opacity<input type="range" id="sub_col_opacity" min="0" max="255"/></label><br/>
|
|
||||||
<button onclick="setTheme()">Apply</button><button onclick="resetTheme(defaultTheme)">Reset</button>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
</details><br/>
|
</details><br/>
|
||||||
|
|
||||||
<details class="ui_win"><summary>goto</summary>
|
<details class="ui_win"><summary>goto</summary>
|
||||||
|
@ -80,6 +68,9 @@
|
||||||
<details class="ui_win"><summary>status</summary><pre id="aux"></pre></details><br/>
|
<details class="ui_win"><summary>status</summary><pre id="aux"></pre></details><br/>
|
||||||
|
|
||||||
<details class="ui_win"><summary>token select</summary>
|
<details class="ui_win"><summary>token select</summary>
|
||||||
|
<button id="tokenPreview_alt_clear" onclick="dismissPreview()" style="display: none;">Dismiss Preview</button>
|
||||||
|
<div id="tokenPreview_alt"></div>
|
||||||
|
<input hidden id="tokenPreview_alt_id"/>
|
||||||
<div id="token_select"></div>
|
<div id="token_select"></div>
|
||||||
</details><br/>
|
</details><br/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,12 +120,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<details class="ui_win" id="theme_accordion"><summary>theme</summary>
|
||||||
|
<form id="theme_cfg" onsubmit="return false">
|
||||||
|
<label>bg color<input type="color" id="bg_col_input"/></label>
|
||||||
|
<label>bg opacity<input type="range" id="bg_col_opacity" min="0" max="255"/></label>
|
||||||
|
<label>fg color<input type="color" id="fg_col_input"/></label>
|
||||||
|
<label>fg opacity<input type="range" id="fg_col_opacity" min="0" max="255"/></label>
|
||||||
|
<label>main color<input type="color" id="main_col_input"/></label>
|
||||||
|
<label>main opacity<input type="range" id="main_col_opacity" min="0" max="255"/></label>
|
||||||
|
<label>sub color<input type="color" id="sub_col_input"/></label>
|
||||||
|
<label>sub opacity<input type="range" id="sub_col_opacity" min="0" max="255"/></label>
|
||||||
|
<label>error color<input type="color" id="err_col_input"/></label>
|
||||||
|
<label>error opacity<input type="range" id="err_col_opacity"/></label>
|
||||||
|
<button onclick="setTheme()">Apply</button><button onclick="resetTheme(defaultTheme)">Reset</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="ui_win" id="felt_info"><a href="https://hacklab.nilfm.cc/felt">felt v0.1.0</a> | built with <a href="https://leafletjs.org">leaflet</a></div>
|
||||||
</nav>
|
</nav>
|
||||||
</body>
|
</body>
|
||||||
<script src="./leaflet.js" type="text/javascript"></script>
|
<script src="./leaflet.js?v=1.9.4" type="text/javascript"></script>
|
||||||
<script src="./util.js" type="text/javascript"></script>
|
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
|
||||||
<script src="./map.js" type="text/javascript"></script>
|
<script src="./map.js?v=0.1.0" type="text/javascript"></script>
|
||||||
<script src="./socket.js" type="text/javascript"></script>
|
<script src="./socket.js?v=0.1.0" type="text/javascript"></script>
|
||||||
<script src="./dice.js" type="text/javascript"></script>
|
<script src="./dice.js?v=0.1.0" type="text/javascript"></script>
|
||||||
<script src="./admin.js" type="text/javascript"></script>
|
<script src="./admin.js?v=0.1.0" type="text/javascript"></script>
|
||||||
</html>
|
</html>
|
|
@ -60,6 +60,11 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-container img.leaflet-tile {
|
||||||
|
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||||
|
mix-blend-mode: plus-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
.leaflet-container.leaflet-touch-zoom {
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
-ms-touch-action: pan-x pan-y;
|
-ms-touch-action: pan-x pan-y;
|
||||||
touch-action: pan-x pan-y;
|
touch-action: pan-x pan-y;
|
||||||
|
@ -423,8 +428,11 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
.leaflet-control-attribution a:focus {
|
.leaflet-control-attribution a:focus {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.leaflet-control-attribution svg {
|
.leaflet-attribution-flag {
|
||||||
display: inline !important;
|
display: inline !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
width: 1em;
|
||||||
|
height: 0.6669em;
|
||||||
}
|
}
|
||||||
.leaflet-left .leaflet-control-scale {
|
.leaflet-left .leaflet-control-scale {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
@ -438,12 +446,10 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
padding: 2px 5px 1px;
|
padding: 2px 5px 1px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
background: #fff;
|
text-shadow: 1px 1px #fff;
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
}
|
||||||
.leaflet-control-scale-line:not(:first-child) {
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
border-top: 2px solid #777;
|
border-top: 2px solid #777;
|
||||||
|
@ -537,8 +543,6 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
}
|
}
|
||||||
.leaflet-popup-scrolled {
|
.leaflet-popup-scrolled {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
@ -647,11 +651,11 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Printing */
|
/* Printing */
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
/* Prevent printers from removing background-images of controls. */
|
/* Prevent printers from removing background-images of controls. */
|
||||||
.leaflet-control {
|
.leaflet-control {
|
||||||
-webkit-print-color-adjust: exact;
|
-webkit-print-color-adjust: exact;
|
||||||
color-adjust: exact;
|
print-color-adjust: exact;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
68
static/logo.svg
Normal file
68
static/logo.svg
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="500"
|
||||||
|
height="350"
|
||||||
|
viewBox="0 0 132.29166 92.604169"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||||
|
sodipodi:docname="logo.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#ffffff"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:pageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
width="500px"
|
||||||
|
inkscape:zoom="0.96886494"
|
||||||
|
inkscape:cx="145.01505"
|
||||||
|
inkscape:cy="248.74468"
|
||||||
|
inkscape:window-width="1600"
|
||||||
|
inkscape:window-height="858"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="24"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#1f9b92;fill-opacity:0.560662;stroke:#1f9b92;stroke-width:1.2686;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
id="rect874-3"
|
||||||
|
width="61.702309"
|
||||||
|
height="61.501461"
|
||||||
|
x="78.612022"
|
||||||
|
y="5.1668525"
|
||||||
|
ry="0"
|
||||||
|
transform="matrix(0.90843196,0.41803275,-0.90682155,0.42151474,0,0)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#1f9b92;fill-opacity:0.560662;stroke:#1f9b92;stroke-width:1.2686;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
id="rect874-3-7"
|
||||||
|
width="61.702309"
|
||||||
|
height="61.501461"
|
||||||
|
x="66.616249"
|
||||||
|
y="-5.9174714"
|
||||||
|
ry="0"
|
||||||
|
transform="matrix(0.90843196,0.41803275,-0.90682155,0.42151474,0,0)" />
|
||||||
|
<circle
|
||||||
|
style="fill:#ffffff;fill-opacity:0.745541;stroke:#1f9b92;stroke-width:1;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
id="path1670"
|
||||||
|
cx="65.443466"
|
||||||
|
cy="12.851727"
|
||||||
|
r="7.798768" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -7,8 +7,8 @@ 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, attributionControl: true, zoomControl: false });
|
map = L.map('map', { minZoom: 0, maxZoom: 4, crs: L.CRS.Simple, attributionControl: false, zoomControl: false });
|
||||||
map.on("zoomend", ()=>{resizeMarkers();scaleSpritePreview();});
|
map.on("zoomend", ()=>{resizeMarkers();scaleSpritePreview();scaleAltSpritePreview();});
|
||||||
}
|
}
|
||||||
if (mapImg) {
|
if (mapImg) {
|
||||||
mapImg.removeFrom(map);
|
mapImg.removeFrom(map);
|
||||||
|
|
|
@ -7,6 +7,10 @@ let table = null;
|
||||||
|
|
||||||
let conn = null;
|
let conn = null;
|
||||||
|
|
||||||
|
const secondaryPreviewZone = document.getElementById("tokenPreview_alt");
|
||||||
|
const secondaryPreviewIdInput = document.getElementById("tokenPreview_alt_id");
|
||||||
|
const dismissPreviewBtn = document.getElementById("tokenPreview_alt_clear");
|
||||||
|
|
||||||
function showTableModal(show) {
|
function showTableModal(show) {
|
||||||
const modal = document.getElementById("table_modal");
|
const modal = document.getElementById("table_modal");
|
||||||
if (modal) {
|
if (modal) {
|
||||||
|
@ -71,12 +75,50 @@ function renderTokenSelect() {
|
||||||
const tokenSelect = document.getElementById("token_select");
|
const tokenSelect = document.getElementById("token_select");
|
||||||
let tokenSelectHTML = "<ul class='single_btn_list'>";
|
let tokenSelectHTML = "<ul class='single_btn_list'>";
|
||||||
for (const t of tokens) {
|
for (const t of tokens) {
|
||||||
tokenSelectHTML += `<li><a target="_blank" href="${t.t.sprite}">${t.t.name}</a><button onclick="toggleActive('${t.t.id}')">${(t.t.active ? "Deactivate" : "Activate")}</button></li>\n`;
|
tokenSelectHTML += `<li><a href="#" onclick="initSpritePreviewById('${t.t.id}')">${t.t.name}</a><button onclick="toggleActive('${t.t.id}')">${(t.t.active ? "Deactivate" : "Activate")}</button></li>\n`;
|
||||||
}
|
}
|
||||||
tokenSelectHTML += "</ul>";
|
tokenSelectHTML += "</ul>";
|
||||||
tokenSelect.innerHTML = tokenSelectHTML;
|
tokenSelect.innerHTML = tokenSelectHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the following few functions aren't socket related but they directly relate to the previous function
|
||||||
|
|
||||||
|
function initSpritePreviewById(id) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
const token = tokens.find(t=>t.t.id == id);
|
||||||
|
|
||||||
|
if (token && id) {
|
||||||
|
img.src = token.t.sprite;
|
||||||
|
secondaryPreviewIdInput.value = id;
|
||||||
|
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
scaleAltSpritePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dismissPreviewBtn.style.display = (token && id) ? "block" : "none";
|
||||||
|
secondaryPreviewZone.innerHTML = "";
|
||||||
|
secondaryPreviewZone.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissPreview() {
|
||||||
|
initSpritePreviewById(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scaleAltSpritePreview() {
|
||||||
|
if (mapImg && mapImg._image) {
|
||||||
|
const id = secondaryPreviewIdInput.value;
|
||||||
|
const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth;
|
||||||
|
const img = secondaryPreviewZone.children[0];
|
||||||
|
const token = tokens.find(t=>t.t.id == id);
|
||||||
|
if (img && token) {
|
||||||
|
img.width = token.t.w * scaleFactor;
|
||||||
|
img.height = token.t.h * scaleFactor;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function makeUpToDate(table) {
|
function makeUpToDate(table) {
|
||||||
if (table) {
|
if (table) {
|
||||||
// map image has to be set before tokens can be handled!
|
// map image has to be set before tokens can be handled!
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
--fg_color: #ccc;
|
--fg_color: #ccc;
|
||||||
--main_color: #1f9b92;
|
--main_color: #1f9b92;
|
||||||
--sub_color: #002b36;
|
--sub_color: #002b36;
|
||||||
|
--err_color: #DC143C;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -66,17 +67,17 @@ button:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
#errWrapper {
|
#errWrapper {
|
||||||
color: #fff;
|
color: var(--err_color);
|
||||||
background: crimson;
|
background: var(--bg_color);
|
||||||
|
border: solid 2px var(--err_color);
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
#closeErr {
|
#closeErr {
|
||||||
display: inline;
|
display: inline;
|
||||||
border: dotted 1px #fff;
|
border: none;
|
||||||
color: #fff;
|
color: var(--err_color);
|
||||||
background: crimson;
|
|
||||||
padding: 0 1ch;
|
padding: 0 1ch;
|
||||||
margin-right: 1ch;
|
margin-right: 1ch;
|
||||||
}
|
}
|
||||||
|
@ -120,6 +121,10 @@ pre {
|
||||||
#adminWrapper {
|
#adminWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.ui_win {
|
.ui_win {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -231,8 +236,26 @@ nav {
|
||||||
color: var(--fg_color);
|
color: var(--fg_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#theme_accordion summary {
|
|
||||||
text-align: right;
|
#theme_accordion {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#felt_info {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme_cfg {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=range] {
|
input[type=range] {
|
||||||
|
@ -304,5 +327,5 @@ input[type=range]::-moz-range-track {
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
border-bottom: solid 2px crimson;
|
color: var(--err_color);
|
||||||
}
|
}
|
|
@ -1,17 +1,19 @@
|
||||||
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 defaultTheme = [ "#000000cc", "#ccccccff", "#1f9b92ff", "#002b36ff", "#DC143Cff" ];
|
||||||
|
|
||||||
const bgCol_input = document.getElementById("bg_col_input");
|
const bgCol_input = document.getElementById("bg_col_input");
|
||||||
const fgCol_input = document.getElementById("fg_col_input");
|
const fgCol_input = document.getElementById("fg_col_input");
|
||||||
const mainCol_input = document.getElementById("main_col_input");
|
const mainCol_input = document.getElementById("main_col_input");
|
||||||
const subCol_input = document.getElementById("sub_col_input");
|
const subCol_input = document.getElementById("sub_col_input");
|
||||||
|
const errCol_input = document.getElementById("err_col_input");
|
||||||
|
|
||||||
const bgOp_input = document.getElementById("bg_col_opacity");
|
const bgOp_input = document.getElementById("bg_col_opacity");
|
||||||
const fgOp_input = document.getElementById("fg_col_opacity");
|
const fgOp_input = document.getElementById("fg_col_opacity");
|
||||||
const mainOp_input = document.getElementById("main_col_opacity");
|
const mainOp_input = document.getElementById("main_col_opacity");
|
||||||
const subOp_input = document.getElementById("sub_col_opacity");
|
const subOp_input = document.getElementById("sub_col_opacity");
|
||||||
|
const errOp_input = document.getElementById("err_col_opacity");
|
||||||
|
|
||||||
const saveData = {
|
const saveData = {
|
||||||
username: "",
|
username: "",
|
||||||
|
@ -82,6 +84,7 @@ function resetTheme(theme) {
|
||||||
let fg_col = theme[1];
|
let fg_col = theme[1];
|
||||||
let main_col = theme[2];
|
let main_col = theme[2];
|
||||||
let sub_col = theme[3];
|
let sub_col = theme[3];
|
||||||
|
let err_col = theme[4];
|
||||||
|
|
||||||
let fg_opacity = hexToBytes(fg_col.substr(7));
|
let fg_opacity = hexToBytes(fg_col.substr(7));
|
||||||
fg_col = fg_col.substr(0,7);
|
fg_col = fg_col.substr(0,7);
|
||||||
|
@ -91,6 +94,8 @@ function resetTheme(theme) {
|
||||||
main_col = main_col.substr(0,7);
|
main_col = main_col.substr(0,7);
|
||||||
let sub_opacity = hexToBytes(sub_col.substr(7));
|
let sub_opacity = hexToBytes(sub_col.substr(7));
|
||||||
sub_col = sub_col.substr(0,7);
|
sub_col = sub_col.substr(0,7);
|
||||||
|
let err_opacity = hexToBytes(err_col.substr(7));
|
||||||
|
err_col = err_col.substr(0,7);
|
||||||
|
|
||||||
bgCol_input.value = bg_col;
|
bgCol_input.value = bg_col;
|
||||||
bgOp_input.value = bg_opacity;
|
bgOp_input.value = bg_opacity;
|
||||||
|
@ -100,6 +105,8 @@ function resetTheme(theme) {
|
||||||
mainOp_input.value = main_opacity;
|
mainOp_input.value = main_opacity;
|
||||||
subCol_input.value = sub_col;
|
subCol_input.value = sub_col;
|
||||||
subOp_input.value = sub_opacity;
|
subOp_input.value = sub_opacity;
|
||||||
|
errCol_input.value = err_col;
|
||||||
|
errOp_input.value = err_opacity;
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,11 +120,14 @@ function setTheme() {
|
||||||
let main_opacity = (toByte(mainOp_input.value));
|
let main_opacity = (toByte(mainOp_input.value));
|
||||||
let sub_col = subCol_input.value;
|
let sub_col = subCol_input.value;
|
||||||
let sub_opacity = (toByte(subOp_input.value));
|
let sub_opacity = (toByte(subOp_input.value));
|
||||||
|
let err_col = errCol_input.value;
|
||||||
|
let err_opacity = (toByte(errOp_input.value));
|
||||||
|
|
||||||
saveData.theme[0] = bg_col + bg_opacity;
|
saveData.theme[0] = bg_col + bg_opacity;
|
||||||
saveData.theme[1] = fg_col + fg_opacity;
|
saveData.theme[1] = fg_col + fg_opacity;
|
||||||
saveData.theme[2] = main_col + main_opacity;
|
saveData.theme[2] = main_col + main_opacity;
|
||||||
saveData.theme[3] = sub_col + sub_opacity;
|
saveData.theme[3] = sub_col + sub_opacity;
|
||||||
|
saveData.theme[4] = err_col + err_opacity;
|
||||||
|
|
||||||
localStorage.setItem("theme", JSON.stringify(saveData.theme));
|
localStorage.setItem("theme", JSON.stringify(saveData.theme));
|
||||||
} catch {} finally {
|
} catch {} finally {
|
||||||
|
@ -125,6 +135,7 @@ function setTheme() {
|
||||||
document.body.style.setProperty("--fg_color", saveData.theme[1]);
|
document.body.style.setProperty("--fg_color", saveData.theme[1]);
|
||||||
document.body.style.setProperty("--main_color", saveData.theme[2]);
|
document.body.style.setProperty("--main_color", saveData.theme[2]);
|
||||||
document.body.style.setProperty("--sub_color", saveData.theme[3]);
|
document.body.style.setProperty("--sub_color", saveData.theme[3]);
|
||||||
|
document.body.style.setProperty("--err_color", saveData.theme[4]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
{{ $params := (.Context).Value "params" }}
|
{{ $params := (.Context).Value "params" }}
|
||||||
|
|
||||||
<html>
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Felt — Error</title>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="shortcut icon" href="/table/favicon.png"/>
|
||||||
|
<link href="/table/style.css?v=0.1.0" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>ERR {{ $params.ErrorCode }}</h1>
|
<main id="registration">
|
||||||
<p>{{ $params.ErrorMessage }}</p>
|
<h1>ERROR {{ $params.ErrorCode }}</h1>
|
||||||
|
<p class="error">{{ $params.ErrorMessage }}</p>
|
||||||
|
<p><a href="/table">Get back to gaming...</a></p>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
|
||||||
|
</html>
|
||||||
|
<html>
|
|
@ -6,7 +6,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Felt — Admin Registration</title>
|
<title>Felt — Admin Registration</title>
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link href="/table/style.css" rel="stylesheet" />
|
<link rel="shortcut icon" href="/table/favicon.png"/>
|
||||||
|
<link href="/table/style.css?v=0.1.0" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="registration">
|
<main id="registration">
|
||||||
|
@ -22,5 +23,5 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script src="/table/util.js" type="text/javascript"></script>
|
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
|
||||||
</html>
|
</html>
|
|
@ -5,7 +5,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Felt — Registration Complete</title>
|
<title>Felt — Registration Complete</title>
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link href="/table/style.css" rel="stylesheet" />
|
<link rel="shortcut icon" href="/table/favicon.png"/>
|
||||||
|
<link href="/table/style.css?v=0.1.0" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="registration">
|
<main id="registration">
|
||||||
|
@ -18,5 +19,5 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script src="/table/util.js" type="text/javascript"></script>
|
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
|
||||||
</html>
|
</html>
|
Loading…
Reference in a new issue