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":
|
||||
if len(args) < 4 {
|
||||
return help()
|
||||
return help(args[0])
|
||||
}
|
||||
userStore.AddUser(args[2], args[3])
|
||||
case "rmuser":
|
||||
if len(args) < 3 {
|
||||
return help()
|
||||
return help(args[0])
|
||||
}
|
||||
userStore.DeleteUser(args[2])
|
||||
case "passwd":
|
||||
if len(args) < 5 {
|
||||
return help()
|
||||
return help(args[0])
|
||||
}
|
||||
userStore.ChangePassword(args[2], args[3], args[4])
|
||||
default:
|
||||
help()
|
||||
return help(args[0])
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hacklab.nilfm.cc/quartzgun/cookie"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
@ -15,6 +17,7 @@ type Config struct {
|
|||
UploadMaxMB int
|
||||
MongoURI string
|
||||
RegistrationSecret string
|
||||
RegistrationSalt []byte
|
||||
}
|
||||
|
||||
func GetConfigLocation() string {
|
||||
|
@ -44,7 +47,15 @@ func ensureConfigLocationExists() {
|
|||
|
||||
func ReadConfig() *Config {
|
||||
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 {
|
||||
|
@ -53,7 +64,7 @@ func (self *Config) Write() error {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
@ -82,24 +93,12 @@ func (self *Config) RunWizard() {
|
|||
fmt.Printf("Max file upload size (MB)? ")
|
||||
self.UploadMaxMB = ensureNumberOption(&inputBuf)
|
||||
|
||||
fmt.Printf("Encryption secret for admin invite codes? ")
|
||||
ensure32BytePassphrase(&inputBuf)
|
||||
self.RegistrationSecret = inputBuf
|
||||
setSecrets([]byte(cookie.GenToken(48)), filepath.Join(GetConfigLocation(), "enclave"))
|
||||
|
||||
fmt.Printf("Configuration complete!\n")
|
||||
fmt.Printf("Configuration complete! Registration secrets have been updated as well!\n")
|
||||
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) {
|
||||
for {
|
||||
fmt.Scanln(buffer)
|
||||
|
@ -161,6 +160,30 @@ func writeConfig(cfg *Config, configFile string) error {
|
|||
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 {
|
||||
f, err := os.ReadFile(configFile)
|
||||
cfg := &Config{}
|
||||
|
|
|
@ -38,7 +38,7 @@ type GameTableServer struct {
|
|||
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{
|
||||
subscribeMessageBuffer: 16,
|
||||
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("/uploads/", http.StripPrefix("/uploads/", renderer.Subtree(uploads)))
|
||||
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("/publish", srvr.publishHandler)
|
||||
|
||||
|
|
4
main.go
4
main.go
|
@ -34,7 +34,7 @@ func run() error {
|
|||
udb := indentalUserDB.CreateIndentalUserDB(
|
||||
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) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ func run() error {
|
|||
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{
|
||||
Handler: gt,
|
||||
ReadTimeout: time.Second * 10,
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -25,12 +24,10 @@ type SymmetricCrypto interface {
|
|||
}
|
||||
|
||||
type SymmetricCrypt struct {
|
||||
Iv []byte
|
||||
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 {
|
||||
stringTimestamp, err := self.Decrypt(cipher)
|
||||
if err != nil {
|
||||
|
@ -62,7 +59,7 @@ func (self *SymmetricCrypt) Encrypt(text string) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
plainText := []byte(text)
|
||||
cfb := cipher.NewCFBEncrypter(block, iv)
|
||||
cfb := cipher.NewCFBEncrypter(block, self.Iv)
|
||||
cipherText := make([]byte, len(plainText))
|
||||
cfb.XORKeyStream(cipherText, plainText)
|
||||
return self.Encode(cipherText), nil
|
||||
|
@ -74,7 +71,7 @@ func (self *SymmetricCrypt) Decrypt(text string) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
cipherText := self.Decode(text)
|
||||
cfb := cipher.NewCFBDecrypter(block, iv)
|
||||
cfb := cipher.NewCFBDecrypter(block, self.Iv)
|
||||
plainText := make([]byte, len(cipherText))
|
||||
cfb.XORKeyStream(plainText, cipherText)
|
||||
return string(plainText), nil
|
||||
|
@ -106,9 +103,8 @@ func WithUserStoreAndCrypto(next http.Handler, udb auth.UserStore, crypto Symmet
|
|||
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"))}
|
||||
crypto := &SymmetricCrypt{Secret: secret}
|
||||
|
||||
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))
|
||||
|
|
|
@ -155,6 +155,7 @@ function scaleSpritePreview(source) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function drawTokenOrigin() {
|
||||
if (tokenSpriteDropdown.selectedIndex >= 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" />
|
||||
<title>Felt</title>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link href="./leaflet.css" rel="stylesheet" />
|
||||
<link href="./style.css" rel="stylesheet" />
|
||||
<link rel="shortcut icon" href="./favicon.png"/>
|
||||
<link href="./leaflet.css?v=1.9.4" rel="stylesheet" />
|
||||
<link href="./style.css?v=0.1.0" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
|
@ -17,19 +18,6 @@
|
|||
<details class="ui_win" open><summary>identity</summary>
|
||||
<label for="name_entry">username</label>
|
||||
<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 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>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>
|
||||
</details><br/>
|
||||
</div>
|
||||
|
@ -129,12 +120,30 @@
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</body>
|
||||
<script src="./leaflet.js" type="text/javascript"></script>
|
||||
<script src="./util.js" type="text/javascript"></script>
|
||||
<script src="./map.js" type="text/javascript"></script>
|
||||
<script src="./socket.js" type="text/javascript"></script>
|
||||
<script src="./dice.js" type="text/javascript"></script>
|
||||
<script src="./admin.js" type="text/javascript"></script>
|
||||
<script src="./leaflet.js?v=1.9.4" type="text/javascript"></script>
|
||||
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
|
||||
<script src="./map.js?v=0.1.0" type="text/javascript"></script>
|
||||
<script src="./socket.js?v=0.1.0" type="text/javascript"></script>
|
||||
<script src="./dice.js?v=0.1.0" type="text/javascript"></script>
|
||||
<script src="./admin.js?v=0.1.0" type="text/javascript"></script>
|
||||
</html>
|
|
@ -60,6 +60,11 @@
|
|||
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 {
|
||||
-ms-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 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-control-attribution svg {
|
||||
.leaflet-attribution-flag {
|
||||
display: inline !important;
|
||||
vertical-align: baseline !important;
|
||||
width: 1em;
|
||||
height: 0.6669em;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
|
@ -438,12 +446,10 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
|||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px #fff;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
|
@ -537,8 +543,6 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
|||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
|
@ -647,11 +651,11 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
|||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-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;
|
||||
if (!map) {
|
||||
init = true;
|
||||
map = L.map('map', { minZoom: 0, maxZoom: 4, crs: L.CRS.Simple, attributionControl: true, zoomControl: false });
|
||||
map.on("zoomend", ()=>{resizeMarkers();scaleSpritePreview();});
|
||||
map = L.map('map', { minZoom: 0, maxZoom: 4, crs: L.CRS.Simple, attributionControl: false, zoomControl: false });
|
||||
map.on("zoomend", ()=>{resizeMarkers();scaleSpritePreview();scaleAltSpritePreview();});
|
||||
}
|
||||
if (mapImg) {
|
||||
mapImg.removeFrom(map);
|
||||
|
|
|
@ -7,6 +7,10 @@ let table = 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) {
|
||||
const modal = document.getElementById("table_modal");
|
||||
if (modal) {
|
||||
|
@ -71,12 +75,50 @@ function renderTokenSelect() {
|
|||
const tokenSelect = document.getElementById("token_select");
|
||||
let tokenSelectHTML = "<ul class='single_btn_list'>";
|
||||
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>";
|
||||
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) {
|
||||
if (table) {
|
||||
// map image has to be set before tokens can be handled!
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
--fg_color: #ccc;
|
||||
--main_color: #1f9b92;
|
||||
--sub_color: #002b36;
|
||||
--err_color: #DC143C;
|
||||
}
|
||||
|
||||
* {
|
||||
|
@ -66,17 +67,17 @@ button:hover {
|
|||
}
|
||||
|
||||
#errWrapper {
|
||||
color: #fff;
|
||||
background: crimson;
|
||||
color: var(--err_color);
|
||||
background: var(--bg_color);
|
||||
border: solid 2px var(--err_color);
|
||||
padding: 1em;
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
}
|
||||
#closeErr {
|
||||
display: inline;
|
||||
border: dotted 1px #fff;
|
||||
color: #fff;
|
||||
background: crimson;
|
||||
border: none;
|
||||
color: var(--err_color);
|
||||
padding: 0 1ch;
|
||||
margin-right: 1ch;
|
||||
}
|
||||
|
@ -120,6 +121,10 @@ pre {
|
|||
#adminWrapper {
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui_win {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
|
@ -231,8 +236,26 @@ nav {
|
|||
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] {
|
||||
|
@ -304,5 +327,5 @@ input[type=range]::-moz-range-track {
|
|||
}
|
||||
|
||||
.error {
|
||||
border-bottom: solid 2px crimson;
|
||||
color: var(--err_color);
|
||||
}
|
|
@ -1,17 +1,19 @@
|
|||
const errDiv = document.getElementById("errDiv");
|
||||
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 fgCol_input = document.getElementById("fg_col_input");
|
||||
const mainCol_input = document.getElementById("main_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 fgOp_input = document.getElementById("fg_col_opacity");
|
||||
const mainOp_input = document.getElementById("main_col_opacity");
|
||||
const subOp_input = document.getElementById("sub_col_opacity");
|
||||
const errOp_input = document.getElementById("err_col_opacity");
|
||||
|
||||
const saveData = {
|
||||
username: "",
|
||||
|
@ -82,6 +84,7 @@ function resetTheme(theme) {
|
|||
let fg_col = theme[1];
|
||||
let main_col = theme[2];
|
||||
let sub_col = theme[3];
|
||||
let err_col = theme[4];
|
||||
|
||||
let fg_opacity = hexToBytes(fg_col.substr(7));
|
||||
fg_col = fg_col.substr(0,7);
|
||||
|
@ -91,6 +94,8 @@ function resetTheme(theme) {
|
|||
main_col = main_col.substr(0,7);
|
||||
let sub_opacity = hexToBytes(sub_col.substr(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;
|
||||
bgOp_input.value = bg_opacity;
|
||||
|
@ -100,6 +105,8 @@ function resetTheme(theme) {
|
|||
mainOp_input.value = main_opacity;
|
||||
subCol_input.value = sub_col;
|
||||
subOp_input.value = sub_opacity;
|
||||
errCol_input.value = err_col;
|
||||
errOp_input.value = err_opacity;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
@ -113,11 +120,14 @@ function setTheme() {
|
|||
let main_opacity = (toByte(mainOp_input.value));
|
||||
let sub_col = subCol_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[1] = fg_col + fg_opacity;
|
||||
saveData.theme[2] = main_col + main_opacity;
|
||||
saveData.theme[3] = sub_col + sub_opacity;
|
||||
saveData.theme[4] = err_col + err_opacity;
|
||||
|
||||
localStorage.setItem("theme", JSON.stringify(saveData.theme));
|
||||
} catch {} finally {
|
||||
|
@ -125,6 +135,7 @@ function setTheme() {
|
|||
document.body.style.setProperty("--fg_color", saveData.theme[1]);
|
||||
document.body.style.setProperty("--main_color", saveData.theme[2]);
|
||||
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" }}
|
||||
|
||||
<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>
|
||||
<h1>ERR {{ $params.ErrorCode }}</h1>
|
||||
<p>{{ $params.ErrorMessage }}</p>
|
||||
<main id="registration">
|
||||
<h1>ERROR {{ $params.ErrorCode }}</h1>
|
||||
<p class="error">{{ $params.ErrorMessage }}</p>
|
||||
<p><a href="/table">Get back to gaming...</a></p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
|
||||
</html>
|
||||
<html>
|
|
@ -6,7 +6,8 @@
|
|||
<meta charset="UTF-8" />
|
||||
<title>Felt — Admin Registration</title>
|
||||
<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>
|
||||
<body>
|
||||
<main id="registration">
|
||||
|
@ -22,5 +23,5 @@
|
|||
{{end}}
|
||||
</main>
|
||||
</body>
|
||||
<script src="/table/util.js" type="text/javascript"></script>
|
||||
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
|
||||
</html>
|
|
@ -5,7 +5,8 @@
|
|||
<meta charset="UTF-8" />
|
||||
<title>Felt — Registration Complete</title>
|
||||
<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>
|
||||
<body>
|
||||
<main id="registration">
|
||||
|
@ -18,5 +19,5 @@
|
|||
{{end}}
|
||||
</main>
|
||||
</body>
|
||||
<script src="/table/util.js" type="text/javascript"></script>
|
||||
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
|
||||
</html>
|
Loading…
Reference in a new issue