autogenerate registration secret/IV, add error color to theme, upgrade leaflet, and add left-hand token preview

This commit is contained in:
Iris Lightshard 2023-07-11 23:57:06 -06:00
parent 7cdcdf5528
commit d82b22f621
Signed by: Iris Lightshard
GPG key ID: 3B7FBC22144E6398
18 changed files with 287 additions and 86 deletions

View file

@ -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
} }

View file

@ -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{}

View file

@ -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)

View file

@ -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 := &register.SymmetricCrypt{Secret: cfg.RegistrationSecret} crypto := &register.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,

View file

@ -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))

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -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>

View file

@ -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
View 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

View file

@ -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);

View file

@ -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!

View file

@ -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);
} }

View file

@ -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]);
} }
} }

View file

@ -1,8 +1,20 @@
{{ $params := (.Context).Value "params" }} {{ $params := (.Context).Value "params" }}
<html> <html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>Felt &mdash; 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>

View file

@ -6,7 +6,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Felt &mdash; Admin Registration</title> <title>Felt &mdash; 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>

View file

@ -5,7 +5,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Felt &mdash; Registration Complete</title> <title>Felt &mdash; 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>