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: nilix
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":
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
}

View file

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

View file

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

View file

@ -34,7 +34,7 @@ func run() error {
udb := indentalUserDB.CreateIndentalUserDB(
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) {
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,

View file

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

View file

@ -155,6 +155,7 @@ function scaleSpritePreview(source) {
}
}
function drawTokenOrigin() {
if (tokenSpriteDropdown.selectedIndex >= 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" />
<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>

View file

@ -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 {
@ -652,6 +656,6 @@ svg.leaflet-image-layer.leaflet-interactive path {
/* 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
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;
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);

View file

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

View file

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

View file

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

View file

@ -1,8 +1,20 @@
{{ $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>
<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>
<script src="./util.js?v=0.1.0" type="text/javascript"></script>
</html>
<html>

View file

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

View file

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