diff --git a/.gitignore b/.gitignore index 131f700..392aa51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,10 @@ node_modules/ -passwordreset*.json mail/*.html -scss/*.css* -scss/bs4/*.css* -scss/bs5/*.css* -data/static/*.css -data/static/*.js -data/static/*.js.map -data/static/ts/ -data/static/modules/ -!data/static/setup.js -data/config-base.json -data/config-default.ini -data/*.html -data/*.txt -data/docs/ -dist/* -jfa-go +dist/ build/ -pkg/ -old/ +data/ version.go notes docs/* +config-payload.json !docs/go.mod diff --git a/.goreleaser.yml b/.goreleaser.yml index f07b59b..1551ec0 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -7,14 +7,20 @@ release: before: hooks: - go mod download + - rm -rf data/web + - mkdir -p data + - cp -r static data/web + - cp -r css data/web/ + - npm install + - cp node_modules/a17t/dist/a17t.css data/web/css/ + - cp node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 data/web/css/ + - cp -r html data/ + - cp -r lang data/ - python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json - python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini - - python3 -m pip install libsass - - npm install - - python3 scss/compile.py - - python3 mail/generate.py + - python3 mail/generate.py -o data/ - python3 version.py {{.Version}} version.go - - bash -c 'npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify' + - bash -c 'npx esbuild ts/*.ts ts/modules/*.ts --outdir=./data/web/js/ --minify' - go get -u github.com/swaggo/swag/cmd/swag - swag init -g main.go builds: @@ -37,9 +43,8 @@ archives: amd64: x86_64 files: - data/* - - data/templates/* - - data/static/* - - data/static/modules/* + - data/**/* + - data/**/**/* checksum: name_template: 'checksums.txt' snapshot: diff --git a/Dockerfile b/Dockerfile index ea15e75..7ddbe32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN apt update -y \ && (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \ && apt install nodejs \ && (cd /opt/build; make all; make compress) \ - && sed -i 's#id="pwrJfPath" placeholder="Folder"#id="pwrJfPath" value="/jf" disabled#g' /opt/build/build/data/templates/setup.html + && sed -i 's#id="pwrJfPath" placeholder="Folder"#id="pwrJfPath" value="/jf" disabled#g' /opt/build/build/data/html/setup.html FROM golang:latest diff --git a/Makefile b/Makefile index d126cfb..6927345 100644 --- a/Makefile +++ b/Makefile @@ -1,33 +1,30 @@ +npm: + $(info installing npm dependencies) + npm install + configuration: $(info Fixing config-base) - python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json + -mkdir -p build/data + python3 config/fixconfig.py -i config/config-base.json -o build/data/config-base.json $(info Generating config-default.ini) - python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini - -sass: - $(info Getting libsass) - python3 -m pip install libsass - $(info Getting node dependencies) - npm install - $(info Compiling sass) - python3 scss/compile.py + python3 config/generate_ini.py -i config/config-base.json -o build/data/config-default.ini email: $(info Generating email html) - python3 mail/generate.py + python3 mail/generate.py -o build/data/ -typescript: - $(info Compiling typescript) - npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify - -rm -r data/static/ts - -rm -r data/static/typings - -rm data/static/*.map +ts: + $(info compiling typescript) + -mkdir -p build/data/web/js + -npx esbuild ts/*.ts ts/modules/*.ts --outdir=./build/data/web/js/ ts-debug: - -npx tsc -p ts/ --sourceMap - -rm -r data/static/ts - -rm -r data/static/typings - cp -r ts data/static/ + $(info compiling typescript w/ sourcemaps) + -mkdir -p build/data/web/js + -npx esbuild ts/*.ts ts/modules/*.ts --sourcemap --outdir=./build/data/web/js/ + -rm -r build/data/web/js/ts + $(info copying typescript) + cp -r ts build/data/web/js swagger: go get github.com/swaggo/swag/cmd/swag @@ -47,11 +44,22 @@ compress: upx --lzma build/jfa-go copy: - $(info Copying data) - cp -r data build/ + $(info copying css) + -mkdir -p build/data/web/css + cp -r css build/data/web/ + cp node_modules/a17t/dist/a17t.css build/data/web/css/ + cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 build/data/web/css/ + $(info copying html) + cp -r html build/data/ + $(info copying static data) + -mkdir -p build/data/web + cp -r static/* build/data/web/ + $(info copying language files) + cp -r lang build/data/ + install: cp -r build $(DESTDIR)/jfa-go -all: configuration sass email version typescript swagger compile copy -debug: configuration sass email version ts-debug swagger compile copy +all: configuration npm email version ts swagger compile copy +debug: configuration npm email version ts-debug swagger compile copy diff --git a/README.md b/README.md index d52f9b6..187d999 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ![jfa-go](data/static/banner.svg) +# ![jfa-go](images/banner.svg) jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) that provides invite-based account creation as well as other features that make one's instance much easier to manage. @@ -21,16 +21,15 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly * 🌓 Customizable look * Specify contact and help messages to appear in emails and pages * Light and dark themes available - * Optionally provide custom CSS ## Interface
- +
- - + +
#### Install diff --git a/README.md.old b/README.md.old new file mode 100644 index 0000000..c27a77e --- /dev/null +++ b/README.md.old @@ -0,0 +1,38 @@ +This branch is for experimenting with [a17t](https://a17t.miles.land/) to replace bootstrap. Page structure is pretty much done (except setup.html), so i'm currently integrating this with the main app and existing web code. + +#### todo +**general** +* [x] modal implementation +* [x] animations +* [x] utilities +* [x] CSS for light & dark + +**admin** +* [x] invites tab +* [x] accounts tab +* [x] settings tab +* [x] modals +* [ ] integration with existing code + +**invites** +* [x] page design +* [ ] integration with existing code + +#### screenshots +##### dark ++ + + + + +
+ +##### light ++ + + + + +
diff --git a/api.go b/api.go index e01920f..9c46b78 100644 --- a/api.go +++ b/api.go @@ -667,6 +667,9 @@ func (app *appContext) DeleteProfile(gc *gin.Context) { gc.BindJSON(&req) name := req.Name if _, ok := app.storage.profiles[name]; ok { + if app.storage.defaultProfile == name { + app.storage.defaultProfile = "" + } delete(app.storage.profiles, name) } app.storage.storeProfiles() @@ -1072,13 +1075,14 @@ func (app *appContext) ApplySettings(gc *gin.Context) { // @Summary Get jfa-go configuration. // @Produce json -// @Success 200 {object} configDTO "Uses the same format as config-base.json" +// @Success 200 {object} settings "Uses the same format as config-base.json" // @Router /config [get] // @Security Bearer // @tags Configuration func (app *appContext) GetConfig(gc *gin.Context) { app.info.Println("Config requested") - resp := map[string]interface{}{} + resp := app.configBase + // Load language options langPath := filepath.Join(app.localPath, "lang", "form") app.lang.langFiles, _ = ioutil.ReadDir(langPath) app.lang.langOptions = make([]string, len(app.lang.langFiles)) @@ -1095,37 +1099,25 @@ func (app *appContext) GetConfig(gc *gin.Context) { app.lang.langOptions[i] = meta.(map[string]interface{})["name"].(string) } } - for section, settings := range app.configBase { - if section == "order" { - resp[section] = settings.([]interface{}) - } else { - resp[section] = make(map[string]interface{}) - for key, values := range settings.(map[string]interface{}) { - if key == "order" { - resp[section].(map[string]interface{})[key] = values.([]interface{}) - } else { - resp[section].(map[string]interface{})[key] = values.(map[string]interface{}) - if key != "meta" { - dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string) - configKey := app.config.Section(section).Key(key) - if dataType == "number" { - if val, err := configKey.Int(); err == nil { - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val - } - } else if dataType == "bool" { - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.MustBool(false) - } else if dataType == "select" && key == "language" { - resp[section].(map[string]interface{})[key].(map[string]interface{})["options"] = app.lang.langOptions - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = app.lang.langOptions[app.lang.chosenIndex] - } else { - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.String() - } - } - } + s := resp.Sections["ui"].Settings["language"] + for sectName, section := range resp.Sections { + for settingName, setting := range section.Settings { + val := app.config.Section(sectName).Key(settingName) + s := resp.Sections[sectName].Settings[settingName] + switch setting.Type { + case "text", "email", "select", "password": + s.Value = val.MustString("") + case "number": + s.Value = val.MustInt(0) + case "bool": + s.Value = val.MustBool(false) } + resp.Sections[sectName].Settings[settingName] = s } } - // resp["jellyfin"].(map[string]interface{})["language"].(map[string]interface{})["options"].([]string) + s.Options = app.lang.langOptions + s.Value = app.lang.langOptions[app.lang.chosenIndex] + resp.Sections["ui"].Settings["language"] = s gc.JSON(200, resp) } @@ -1176,11 +1168,11 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { if _, ok := req["password_validation"]; ok { app.debug.Println("Reinitializing validator") validatorConf := ValidatorConf{ - "characters": app.config.Section("password_validation").Key("min_length").MustInt(0), - "uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0), - "lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0), - "numbers": app.config.Section("password_validation").Key("number").MustInt(0), - "special characters": app.config.Section("password_validation").Key("special").MustInt(0), + "length": app.config.Section("password_validation").Key("min_length").MustInt(0), + "uppercase": app.config.Section("password_validation").Key("upper").MustInt(0), + "lowercase": app.config.Section("password_validation").Key("lower").MustInt(0), + "number": app.config.Section("password_validation").Key("number").MustInt(0), + "special": app.config.Section("password_validation").Key("special").MustInt(0), } if !app.config.Section("password_validation").Key("enabled").MustBool(false) { for key := range validatorConf { diff --git a/config/config-base.json b/config/config-base.json index 21e95a2..9d5c920 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -1,668 +1,690 @@ { - "jellyfin": { - "meta": { - "name": "Jellyfin", - "description": "Settings for connecting to Jellyfin" + "order": [], + "sections": { + "jellyfin": { + "order": [], + "meta": { + "name": "Jellyfin", + "description": "Settings for connecting to Jellyfin" + }, + "settings": { + "username": { + "name": "Jellyfin Username", + "required": true, + "requires_restart": true, + "type": "text", + "value": "username", + "description": "It is recommended to create a limited admin account for this program." + }, + "password": { + "name": "Jellyfin Password", + "required": true, + "requires_restart": true, + "type": "password", + "value": "password" + }, + "server": { + "name": "Server address", + "required": true, + "requires_restart": true, + "type": "text", + "value": "http://jellyfin.local:8096", + "description": "Jellyfin server address. Can be public, or local for security purposes." + }, + "public_server": { + "name": "Public address", + "required": false, + "requires_restart": false, + "type": "text", + "value": "https://jellyf.in:443", + "description": "Publicly accessible Jellyfin address for invite form. Leave blank to reuse the above address." + }, + "client": { + "name": "Client Name", + "required": true, + "requires_restart": true, + "type": "text", + "value": "jfa-go", + "description": "The name of the client that will show up in the Jellyfin dashboard." + }, + "cache_timeout": { + "name": "User cache timeout (minutes)", + "required": false, + "requires_restart": true, + "type": "number", + "value": 30, + "description": "Timeout of user cache in minutes. Set to 0 to disable." + } + } }, - "username": { - "name": "Jellyfin Username", - "required": true, - "requires_restart": true, - "type": "text", - "value": "username", - "description": "It is recommended to create a limited admin account for this program." + "ui": { + "order": [], + "meta": { + "name": "General", + "description": "Settings related to the UI and program functionality." + }, + "settings": { + "language": { + "name": "Language", + "required": false, + "requires_restart": true, + "type": "select", + "options": [ + "en-us" + ], + "value": "en-US", + "description": "UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate." + }, + "theme": { + "name": "Default Look", + "required": false, + "requires_restart": true, + "type": "select", + "options": [ + "Jellyfin (Dark)", + "Default (Light)" + ], + "value": "Jellyfin (Dark)", + "description": "Default appearance for all users." + }, + "host": { + "name": "Address", + "required": true, + "requires_restart": true, + "type": "text", + "value": "0.0.0.0", + "description": "Set 0.0.0.0 to run on localhost" + }, + "port": { + "name": "Port", + "required": true, + "requires_restart": true, + "type": "number", + "value": 8056 + }, + "jellyfin_login": { + "name": "Use Jellyfin for authentication", + "required": false, + "requires_restart": true, + "type": "bool", + "value": true, + "description": "Enable this to use Jellyfin users instead of the below username and pw." + }, + "admin_only": { + "name": "Allow admin users only", + "required": false, + "requires_restart": true, + "depends_true": "jellyfin_login", + "type": "bool", + "value": true, + "description": "Allows only admin users on Jellyfin to access the admin page." + }, + "username": { + "name": "Web Username", + "required": true, + "requires_restart": true, + "depends_false": "jellyfin_login", + "type": "text", + "value": "your username", + "description": "Username for admin page (Leave blank if using jellyfin_login)" + }, + "password": { + "name": "Web Password", + "required": true, + "requires_restart": true, + "depends_false": "jellyfin_login", + "type": "password", + "value": "your password", + "description": "Password for admin page (Leave blank if using jellyfin_login)" + }, + "email": { + "name": "Admin email address", + "required": false, + "requires_restart": false, + "depends_false": "jellyfin_login", + "type": "text", + "value": "example@example.com", + "description": "Address to send notifications to (Leave blank if using jellyfin_login)" + }, + "debug": { + "name": "Debug logging", + "required": false, + "requires_restart": true, + "type": "bool", + "value": false, + "description": "Enables debug logging and exposes pprof as a route (Don't use in production!)" + }, + "contact_message": { + "name": "Contact message", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Need help? contact me.", + "description": "Displayed at bottom of all pages except admin" + }, + "help_message": { + "name": "Help message", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Enter your details to create an account.", + "description": "Displayed at top of invite form." + }, + "success_message": { + "name": "Success message", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Your account has been created. Click below to continue to Jellyfin.", + "description": "Displayed when a user creates an account" + }, + "url_base": { + "name": "URL Base", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "URL base for when running jfa-go with a reverse proxy in a subfolder." + } + } }, - "password": { - "name": "Jellyfin Password", - "required": true, - "requires_restart": true, - "type": "password", - "value": "password" - }, - "server": { - "name": "Server address", - "required": true, - "requires_restart": true, - "type": "text", - "value": "http://jellyfin.local:8096", - "description": "Jellyfin server address. Can be public, or local for security purposes." - }, - "public_server": { - "name": "Public address", - "required": false, - "requires_restart": false, - "type": "text", - "value": "https://jellyf.in:443", - "description": "Publicly accessible Jellyfin address for invite form. Leave blank to reuse the above address." - }, - "client": { - "name": "Client Name", - "required": true, - "requires_restart": true, - "type": "text", - "value": "jfa-go", - "description": "The name of the client that will show up in the Jellyfin dashboard." - }, - "cache_timeout": { - "name": "User cache timeout (minutes)", - "required": false, - "requires_restart": true, - "type": "number", - "value": 30, - "description": "Timeout of user cache in minutes. Set to 0 to disable." - } - }, - "ui": { - "meta": { - "name": "General", - "description": "Settings related to the UI and program functionality." - }, - "language": { - "name": "Language", - "required": false, - "requires_restart": true, - "type": "select", - "options": [ - "en-us" - ], - "value": "en-US", - "description": "UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate." - }, - "theme": { - "name": "Default Look", - "required": false, - "requires_restart": true, - "type": "select", - "options": [ - "Bootstrap (Light)", - "Jellyfin (Dark)", - "Custom CSS" - ], - "value": "Jellyfin (Dark)", - "description": "Default appearance for all users." - }, - "host": { - "name": "Address", - "required": true, - "requires_restart": true, - "type": "text", - "value": "0.0.0.0", - "description": "Set 0.0.0.0 to run on localhost" - }, - "port": { - "name": "Port", - "required": true, - "requires_restart": true, - "type": "number", - "value": 8056 - }, - "jellyfin_login": { - "name": "Use Jellyfin for authentication", - "required": false, - "requires_restart": true, - "type": "bool", - "value": true, - "description": "Enable this to use Jellyfin users instead of the below username and pw." - }, - "admin_only": { - "name": "Allow admin users only", - "required": false, - "requires_restart": true, - "depends_true": "jellyfin_login", - "type": "bool", - "value": true, - "description": "Allows only admin users on Jellyfin to access the admin page." - }, - "username": { - "name": "Web Username", - "required": true, - "requires_restart": true, - "depends_false": "jellyfin_login", - "type": "text", - "value": "your username", - "description": "Username for admin page (Leave blank if using jellyfin_login)" - }, - "password": { - "name": "Web Password", - "required": true, - "requires_restart": true, - "depends_false": "jellyfin_login", - "type": "password", - "value": "your password", - "description": "Password for admin page (Leave blank if using jellyfin_login)" + "password_validation": { + "order": [], + "meta": { + "name": "Password Validation", + "description": "Password validation (minimum length, etc.)" + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": false, + "type": "bool", + "value": true + }, + "min_length": { + "name": "Minimum Length", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "8" + }, + "upper": { + "name": "Minimum uppercase characters", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "1" + }, + "lower": { + "name": "Minimum lowercase characters", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "0" + }, + "number": { + "name": "Minimum number count", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "1" + }, + "special": { + "name": "Minimum number of special characters", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "0" + } + } }, "email": { - "name": "Admin email address", - "required": false, - "requires_restart": false, - "depends_false": "jellyfin_login", - "type": "text", - "value": "example@example.com", - "description": "Address to send notifications to (Leave blank if using jellyfin_login)" + "order": [], + "meta": { + "name": "Email", + "description": "General email settings. Ignore if not using email features." + }, + "settings": { + "no_username": { + "name": "Use email addresses as username", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "bool", + "value": false, + "description": "Use email address from invite form as username on Jellyfin." + }, + "use_24h": { + "name": "Use 24h time", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "bool", + "value": true + }, + "date_format": { + "name": "Date format", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "text", + "value": "%d/%m/%y", + "description": "Date format used in emails. Follows datetime.strftime format." + }, + "message": { + "name": "Help message", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "text", + "value": "Need help? contact me.", + "description": "Message displayed at bottom of emails." + }, + "method": { + "name": "Email method", + "required": false, + "requires_restart": false, + "type": "select", + "options": [ + "smtp", + "mailgun" + ], + "value": "smtp", + "description": "Method of sending email to use." + }, + "address": { + "name": "Sent from (address)", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "email", + "value": "jellyfin@jellyf.in", + "description": "Address to send emails from" + }, + "from": { + "name": "Sent from (name)", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "text", + "value": "Jellyfin", + "description": "The name of the sender" + } + } }, - "debug": { - "name": "Debug logging", - "required": false, - "requires_restart": true, - "type": "bool", - "value": false, - "description": "Enables debug logging and exposes pprof as a route (Don't use in production!)" + "password_resets": { + "order": [], + "meta": { + "name": "Password Resets", + "description": "Settings for the password reset handler." + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": true, + "type": "bool", + "value": true, + "description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins" + }, + "watch_directory": { + "name": "Jellyfin directory", + "required": false, + "requires_restart": true, + "depends_true": "enabled", + "type": "text", + "value": "/path/to/jellyfin", + "description": "Path to the folder Jellyfin puts password-reset files." + }, + "email_html": { + "name": "Custom email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email html" + }, + "email_text": { + "name": "Custom email (plaintext)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email in plain text" + }, + "subject": { + "name": "Email subject", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "Password Reset - Jellyfin", + "description": "Subject of password reset emails." + } + } }, - "contact_message": { - "name": "Contact message", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Need help? contact me.", - "description": "Displayed at bottom of all pages except admin" + "invite_emails": { + "order": [], + "meta": { + "name": "Invite emails", + "description": "Settings for sending invites directly to users." + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": false, + "type": "bool", + "value": true + }, + "email_html": { + "name": "Custom email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email HTML" + }, + "email_text": { + "name": "Custom email (plaintext)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email in plain text" + }, + "subject": { + "name": "Email subject", + "required": true, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "Invite - Jellyfin", + "description": "Subject of invite emails." + }, + "url_base": { + "name": "URL Base", + "required": true, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "http://accounts.jellyf.in:8056/invite", + "description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself." + } + } }, - "help_message": { - "name": "Help message", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Enter your details to create an account.", - "description": "Displayed at top of invite form." + "notifications": { + "order": [], + "meta": { + "name": "Notifications", + "description": "Notification related settings." + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": "false", + "requires_restart": true, + "type": "bool", + "value": true, + "description": "Enabling adds optional toggles to invites to notify on expiry and user creation." + }, + "expiry_html": { + "name": "Expiry email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to expiry notification email HTML." + }, + "expiry_text": { + "name": "Expiry email (Plaintext)", + "required": false, + "requires_restart": "false", + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to expiry notification email in plaintext." + }, + "created_html": { + "name": "User created email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to user creation notification email HTML." + }, + "created_text": { + "name": "User created email (Plaintext)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to user creation notification email in plaintext." + } + } }, - "success_message": { - "name": "Success message", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Your account has been created. Click below to continue to Jellyfin.", - "description": "Displayed when a user creates an account" + "mailgun": { + "order": [], + "meta": { + "name": "Mailgun (Email)", + "description": "Mailgun API connection settings" + }, + "settings": { + "api_url": { + "name": "API URL", + "required": false, + "requires_restart": false, + "type": "text", + "value": "https://api.mailgun.net..." + }, + "api_key": { + "name": "API Key", + "required": false, + "requires_restart": false, + "type": "text", + "value": "your api key" + } + } }, - "bs5": { - "name": "Use Bootstrap 5", - "required": false, - "requires_restart": true, - "type": "bool", - "value": false, - "description": "Use the Bootstrap 5 Alpha. Looks better and removes the need for jQuery, so the page should load faster." + "smtp": { + "order": [], + "meta": { + "name": "SMTP (Email)", + "description": "SMTP Server connection settings." + }, + "settings": { + "username": { + "name": "Username", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Username for SMTP. Leave blank to user send from address as username." + }, + "encryption": { + "name": "Encryption Method", + "required": false, + "requires_restart": false, + "type": "select", + "options": [ + "ssl_tls", + "starttls" + ], + "value": "starttls", + "description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls." + }, + "server": { + "name": "Server address", + "required": false, + "requires_restart": false, + "type": "text", + "value": "smtp.jellyf.in", + "description": "SMTP Server address." + }, + "port": { + "name": "Port", + "required": false, + "requires_restart": false, + "type": "number", + "value": 465 + }, + "password": { + "name": "Password", + "required": false, + "requires_restart": false, + "type": "password", + "value": "smtp password" + } + } }, - "url_base": { - "name": "URL Base", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "URL base for when running jfa-go with a reverse proxy in a subfolder." - } - }, - "password_validation": { - "meta": { - "name": "Password Validation", - "description": "Password validation (minimum length, etc.)" + "ombi": { + "order": [], + "meta": { + "name": "Ombi Integration", + "description": "Connect to Ombi to automatically create both Ombi and Jellyfin accounts for new users. You'll need to create a user template for this to work. Once enabled, refresh to see an option in settings for this." + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": true, + "type": "bool", + "value": false, + "description": "Enable to create an Ombi account for new Jellyfin users" + }, + "server": { + "name": "URL", + "required": false, + "requires_restart": true, + "type": "text", + "value": "localhost:5000", + "depends_true": "enabled", + "description": "Ombi server URL, including http(s)://." + }, + "api_key": { + "name": "API Key", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "depends_true": "enabled", + "description": "API Key. Get this from the first tab in Ombi settings." + } + } }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": false, - "type": "bool", - "value": true + "deletion": { + "order": [], + "meta": { + "name": "Account Deletion", + "description": "Subject/email files for account deletion emails." + }, + "settings": { + "subject": { + "name": "Email subject", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Your account was deleted - Jellyfin", + "description": "Subject of account deletion emails." + }, + "email_html": { + "name": "Custom email (HTML)", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Path to custom email html" + }, + "email_text": { + "name": "Custom email (plaintext)", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Path to custom email in plain text" + } + } }, - "min_length": { - "name": "Minimum Length", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "8" - }, - "upper": { - "name": "Minimum uppercase characters", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "1" - }, - "lower": { - "name": "Minimum lowercase characters", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "0" - }, - "number": { - "name": "Minimum number count", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "1" - }, - "special": { - "name": "Minimum number of special characters", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "0" - } - }, - "email": { - "meta": { - "name": "Email", - "description": "General email settings. Ignore if not using email features." - }, - "no_username": { - "name": "Use email addresses as username", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "bool", - "value": false, - "description": "Use email address from invite form as username on Jellyfin." - }, - "use_24h": { - "name": "Use 24h time", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "bool", - "value": true - }, - "date_format": { - "name": "Date format", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "text", - "value": "%d/%m/%y", - "description": "Date format used in emails. Follows datetime.strftime format." - }, - "message": { - "name": "Help message", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "text", - "value": "Need help? contact me.", - "description": "Message displayed at bottom of emails." - }, - "method": { - "name": "Email method", - "required": false, - "requires_restart": false, - "type": "select", - "options": [ - "smtp", - "mailgun" - ], - "value": "smtp", - "description": "Method of sending email to use." - }, - "address": { - "name": "Sent from (address)", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "email", - "value": "jellyfin@jellyf.in", - "description": "Address to send emails from" - }, - "from": { - "name": "Sent from (name)", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "text", - "value": "Jellyfin", - "description": "The name of the sender" - } - }, - "password_resets": { - "meta": { - "name": "Password Resets", - "description": "Settings for the password reset handler." - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": true, - "type": "bool", - "value": true, - "description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins" - }, - "watch_directory": { - "name": "Jellyfin directory", - "required": false, - "requires_restart": true, - "depends_true": "enabled", - "type": "text", - "value": "/path/to/jellyfin", - "description": "Path to the folder Jellyfin puts password-reset files." - }, - "email_html": { - "name": "Custom email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email html" - }, - "email_text": { - "name": "Custom email (plaintext)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email in plain text" - }, - "subject": { - "name": "Email subject", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "Password Reset - Jellyfin", - "description": "Subject of password reset emails." - } - }, - "invite_emails": { - "meta": { - "name": "Invite emails", - "description": "Settings for sending invites directly to users." - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": false, - "type": "bool", - "value": true - }, - "email_html": { - "name": "Custom email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email HTML" - }, - "email_text": { - "name": "Custom email (plaintext)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email in plain text" - }, - "subject": { - "name": "Email subject", - "required": true, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "Invite - Jellyfin", - "description": "Subject of invite emails." - }, - "url_base": { - "name": "URL Base", - "required": true, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "http://accounts.jellyf.in:8056/invite", - "description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself." - } - }, - "notifications": { - "meta": { - "name": "Notifications", - "description": "Notification related settings." - }, - "enabled": { - "name": "Enabled", - "required": "false", - "requires_restart": true, - "type": "bool", - "value": true, - "description": "Enabling adds optional toggles to invites to notify on expiry and user creation." - }, - "expiry_html": { - "name": "Expiry email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to expiry notification email HTML." - }, - "expiry_text": { - "name": "Expiry email (Plaintext)", - "required": false, - "requires_restart": "false", - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to expiry notification email in plaintext." - }, - "created_html": { - "name": "User created email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to user creation notification email HTML." - }, - "created_text": { - "name": "User created email (Plaintext)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to user creation notification email in plaintext." - } - }, - "mailgun": { - "meta": { - "name": "Mailgun (Email)", - "description": "Mailgun API connection settings" - }, - "api_url": { - "name": "API URL", - "required": false, - "requires_restart": false, - "type": "text", - "value": "https://api.mailgun.net..." - }, - "api_key": { - "name": "API Key", - "required": false, - "requires_restart": false, - "type": "text", - "value": "your api key" - } - }, - "smtp": { - "meta": { - "name": "SMTP (Email)", - "description": "SMTP Server connection settings." - }, - "username": { - "name": "Username", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Username for SMTP. Leave blank to user send from address as username." - }, - "encryption": { - "name": "Encryption Method", - "required": false, - "requires_restart": false, - "type": "select", - "options": [ - "ssl_tls", - "starttls" - ], - "value": "starttls", - "description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls." - }, - "server": { - "name": "Server address", - "required": false, - "requires_restart": false, - "type": "text", - "value": "smtp.jellyf.in", - "description": "SMTP Server address." - }, - "port": { - "name": "Port", - "required": false, - "requires_restart": false, - "type": "number", - "value": 465 - }, - "password": { - "name": "Password", - "required": false, - "requires_restart": false, - "type": "password", - "value": "smtp password" - } - }, - "ombi": { - "meta": { - "name": "Ombi Integration", - "description": "Connect to Ombi to automatically create both Ombi and Jellyfin accounts for new users. You'll need to create a user template for this to work. Once enabled, refresh to see an option in settings for this." - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": true, - "type": "bool", - "value": false, - "description": "Enable to create an Ombi account for new Jellyfin users" - }, - "server": { - "name": "URL", - "required": false, - "requires_restart": true, - "type": "text", - "value": "localhost:5000", - "depends_true": "enabled", - "description": "Ombi server URL, including http(s)://." - }, - "api_key": { - "name": "API Key", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "depends_true": "enabled", - "description": "API Key. Get this from the first tab in Ombi settings." - } - }, - "deletion": { - "meta": { - "name": "Account Deletion", - "description": "Subject/email files for account deletion emails." - }, - "subject": { - "name": "Email subject", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Your account was deleted - Jellyfin", - "description": "Subject of account deletion emails." - }, - "email_html": { - "name": "Custom email (HTML)", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Path to custom email html" - }, - "email_text": { - "name": "Custom email (plaintext)", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Path to custom email in plain text" - } - }, - "files": { - "meta": { - "name": "File Storage", - "description": "Optional settings for changing storage locations." - }, - "invites": { - "name": "Invite Storage", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of stored invites (json)." - }, - "emails": { - "name": "Email Addresses", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of stored email addresses (json)." - }, - "ombi_template": { - "name": "Ombi user template", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Location of stored Ombi user template." - }, - "user_template": { - "name": "User Template (Deprecated)", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Deprecated in favor of User Profiles. Location of stored user policy template (json)." - }, - "user_configuration": { - "name": "userConfiguration (Deprecated)", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Deprecated in favor of User Profiles. Location of stored user configuration template (used for setting homescreen layout) (json)" - }, - "user_displayprefs": { - "name": "displayPreferences (Deprecated)", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Deprecated in favor of User Profiles. Location of stored displayPreferences template (also used for homescreen layout) (json)" - }, - "user_profiles": { - "name": "User Profiles", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of stored user profiles (encompasses template and configuration and displayprefs) (json)" - }, - "custom_css": { - "name": "Custom CSS", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of custom bootstrap CSS." - }, - "html_templates": { - "name": "Custom HTML Template Directory", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Path to directory containing custom versions of web ui pages. See wiki for more info." + "files": { + "order": [], + "meta": { + "name": "File Storage", + "description": "Optional settings for changing storage locations." + }, + "settings": { + "invites": { + "name": "Invite Storage", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored invites (json)." + }, + "emails": { + "name": "Email Addresses", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored email addresses (json)." + }, + "ombi_template": { + "name": "Ombi user template", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Location of stored Ombi user template." + }, + "user_template": { + "name": "User Template (Deprecated)", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Deprecated in favor of User Profiles. Location of stored user policy template (json)." + }, + "user_configuration": { + "name": "userConfiguration (Deprecated)", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Deprecated in favor of User Profiles. Location of stored user configuration template (used for setting homescreen layout) (json)" + }, + "user_displayprefs": { + "name": "displayPreferences (Deprecated)", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Deprecated in favor of User Profiles. Location of stored displayPreferences template (also used for homescreen layout) (json)" + }, + "user_profiles": { + "name": "User Profiles", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored user profiles (encompasses template and configuration and displayprefs) (json)" + }, + "html_templates": { + "name": "Custom HTML Template Directory", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Path to directory containing custom versions of web ui pages. See wiki for more info." + } + } } } } diff --git a/config/fixconfig.py b/config/fixconfig.py index 180f2e4..2cf90b4 100644 --- a/config/fixconfig.py +++ b/config/fixconfig.py @@ -9,17 +9,17 @@ args = parser.parse_args() with open(args.input, 'r') as f: config = json.load(f) -newconfig = {"order": []} +newconfig = {"sections": {}, "order": []} -for sect in config: +for sect in config["sections"]: newconfig["order"].append(sect) - newconfig[sect] = {} - newconfig[sect]["order"] = [] - newconfig[sect]["meta"] = config[sect]["meta"] - for setting in config[sect]: - if setting != "meta": - newconfig[sect]["order"].append(setting) - newconfig[sect][setting] = config[sect][setting] + newconfig["sections"][sect] = {} + newconfig["sections"][sect]["order"] = [] + newconfig["sections"][sect]["meta"] = config["sections"][sect]["meta"] + newconfig["sections"][sect]["settings"] = {} + for setting in config["sections"][sect]["settings"]: + newconfig["sections"][sect]["order"].append(setting) + newconfig["sections"][sect]["settings"][setting] = config["sections"][sect]["settings"][setting] with open(args.output, 'w') as f: f.write(json.dumps(newconfig, indent=4)) diff --git a/config/generate_ini.py b/config/generate_ini.py index efc8c0a..a801b8b 100644 --- a/config/generate_ini.py +++ b/config/generate_ini.py @@ -14,18 +14,19 @@ def generate_ini(base_file, ini_file): ini = configparser.RawConfigParser(allow_no_value=True) - for section in config_base: + for section in config_base["sections"]: ini.add_section(section) - for entry in config_base[section]: - if "description" in config_base[section][entry]: - ini.set(section, "; " + config_base[section][entry]["description"]) - if entry != "meta": - value = config_base[section][entry]["value"] - if isinstance(value, bool): - value = str(value).lower() - else: - value = str(value) - ini.set(section, entry, value) + if "meta" in config_base["sections"][section]: + ini.set(section, "; " + config_base["sections"][section]["meta"]["description"]) + for entry in config_base["sections"][section]["settings"]: + if "description" in config_base["sections"][section]["settings"][entry]: + ini.set(section, "; " + config_base["sections"][section]["settings"][entry]["description"]) + value = config_base["sections"][section]["settings"][entry]["value"] + if isinstance(value, bool): + value = str(value).lower() + else: + value = str(value) + ini.set(section, entry, value) with open(Path(ini_file), "w") as config_file: ini.write(config_file) diff --git a/css/base.css b/css/base.css new file mode 100644 index 0000000..2b919d2 --- /dev/null +++ b/css/base.css @@ -0,0 +1,372 @@ +@import "a17t.css"; +@import "remixicon.css"; +@import "modal.css"; +@import "dark.css"; +@import "tooltip.css"; +@import "loader.css"; + +:root { + --border-width-default: 2px; + --border-width-2: 3px; + --border-width-4: 5px; + --border-width-8: 8px; +} + +.light-theme { + --settings-section-button-filter: 90%; +} + +.body { + background-color: #101010; +} + +.page-container { + margin: 5% 20% 5% 20%; +} + +@media (max-width: 1100px) { + .page-container { + margin: 2%; + } +} + +@media screen and (max-width: 750px) { + :root { + font-size: 0.9rem; + } + .table-responsive table { + min-width: 660px; + } +} + + +.tab-button { + font-size: 2rem; +} + +.mb-half { + margin-bottom: 0.5rem; +} + +.mb-1 { + margin-bottom: 1rem; +} + +.mb-2 { + margin-bottom: 2rem; +} + +.mt-half { + margin-top: 0.5rem; +} + +.mt-1 { + margin-top: 1rem; +} + +.ml-1 { + margin-left: 1rem; +} + +.ml-half { + margin-left: 0.5rem; +} + +.mr-1 { + margin-right: 1rem; +} + +.pb-1 { + padding-bottom: 1rem; +} + +.pl-1 { + padding-left: 1rem; +} + +.al { + text-align: left; +} + +.ar { + text-align: right; +} + +.inline-block { + display: inline-block; +} + +.align-top { + align-items: top; +} + +.flex { + display: flex; +} + +.flex-expand { + display: flex; + justify-content: space-between; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-row-group { + display: block; + flex-grow: 1; +} + +.row { + display: flex; + flex-wrap: wrap; +} + +.row.baseline { + align-items: baseline; +} + +.col { + flex: 1; + margin: 0.5rem; +} + +@media screen and (max-width: 400px) { + .row { + flex-direction: column; + } + .col { + flex: 45%; + } +} + +.fr { + float: right; +} + +.monospace { + background-color: inherit; /* so we can use a17t code blocks */ +} + +sup.\~critical, .text-critical { + color: var(--color-critical-normal-content); +} + +.grey { + color: var(--color-neutral-500); +} + +.aside.sm { + font-size: 0.8rem; + padding: 0.8rem; +} + +.support.lg { + font-size: 1rem; +} + +.badge.lg { + font-size: 1rem; +} + +.inv-created-users strong,p { + padding-left: 0.5rem; + padding-bottom: 0.2rem; +} + +.inv-created-users.empty strong,p { + padding: 0; +} + +.inv { + overflow: visible; +} + +.inv-table { + font-size: 0.8rem; +} + +.inv-profilearea { + min-width: 20%; +} + +.inv-profileselect { + min-width: 100%; +} + +.inv-codearea { + max-width: 40%; + min-width: 10rem; + display: flex; + justify-content: center; + align-items: center; +} + +.inv-empty .inv-codearea { + justify-content: start; +} + + +.invite-link { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: auto; +} + +.ellipsis { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.no-pad { + padding: 0px 0px 0px 0px; +} + +.elem-pad > * { + margin: var(--spacing-4, 1rem); +} + +.icon.clickable { + padding: 0.5rem 0.6rem; +} + +.input { + box-sizing: border-box; /* fixes weird length issue with inputs */ +} + +.button.lg { + height: 2.5rem; +} + +.submit { + border: none; + outline: none; /* remove browser styling on submit buttons */ +} + +.full-width { /* full width inputs */ + box-sizing: border-box; /* TODO: maybe remove if we figure out input thing? */ + width: 100%; +} + +.center { + justify-content: center; +} + +.no-lp { + padding-left: 0px; +} + +.block { + display: block; +} + +.focused { + display: block; +} + +.unfocused { + display: none; +} + +.rotated { + transform: rotate(180deg); + -webkit-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + -moz-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + -o-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + transition: all 0.3s cubic-bezier(0,.89,.27,.92); +} + +.not-rotated { + transform: rotate(0deg); + -webkit-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + -moz-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + -o-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + transition: all 0.3s cubic-bezier(0,.89,.27,.92); +} + +.stealth-input { + font-size: 1rem; + padding-top: 0.1rem; + padding-bottom: 0.1rem; + margin-left: 0.5rem; + margin-right: 1rem; + max-width: 75%; +} + +.stealth-input-hidden { + border-style: none; + --fallback-box-shadow: none; + --field-hover-box-shadow: none; + --field-focus-box-shadow: none; + padding-top: 0.1rem; + padding-bottom: 0.1rem; +} + +.settings-section-button { + box-sizing: border-box; + width: 100%; + height: 2.5rem; + background-color: rgba(0,0,0,0); +} + +.settings-section-button:hover, .settings-section-button:focus { + box-sizing: border-box; + width: 100%; + height: 2.5rem; + background-color: var(--color-neutral-normal-fill); + filter: brightness(var(--settings-section-button-filter)) !important; +} + +.settings-section-button.selected { + background-color: var(--color-neutral-normal-fill); + --buton-filter-brightness: var(--settings-section-button-filter); + filter: brightness(var(--settings-section-button-filter)) !important; +} + +.setting { + margin-bottom: 0.25rem; +} + +.textarea { + resize: vertical; +} + +.overflow { + overflow: visible; +} + +.overflow-y { + overflow-y: visible; +} + +select, textarea { + color: inherit; + border: 0 solid var(--color-neutral-300); + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; +} + +input { + color: inherit; + border: 0 solid var(--color-neutral-300); +} + +p.top { + margin-top: 0px; +} + +.table-responsive { + overflow-x: auto; +} + +#notification-box { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 16; +} diff --git a/css/dark.css b/css/dark.css new file mode 100644 index 0000000..b21de54 --- /dev/null +++ b/css/dark.css @@ -0,0 +1,87 @@ +.dark-theme { + + --settings-section-button-filter: 110%; + + --color-neutral-900: rgba(255, 255, 255, 0.87); + --color-neutral-800: rgba(255, 255, 255, 0.8); + --color-neutral-700: rgba(255, 255, 255, 0.73); + --color-neutral-600: rgba(255, 255, 255, 0.66); + --color-neutral-500: rgb(153, 153, 153); + --color-neutral-400: #383838; + --color-neutral-300: #303030; + --color-neutral-200: #292929; + --color-neutral-100: #242424; + --color-neutral-50: #202020; + --color-neutral-000: #101010; + + --color-critical-900: #fef2f2; + --color-critical-800: #fee2e2; + --color-critical-700: #fecaca; + --color-critical-600: #fca5a5; + --color-critical-500: #f87171; + --color-critical-400: #ef4444; + --color-critical-300: #dc2626; + --color-critical-200: #b91c1c; + --color-critical-100: #991b1b; + --color-critical-50: #7f1d1d; + --color-critical-000: #441313; + + --color-warning-900: #fffbeb; + --color-warning-800: #fef3c7; + --color-warning-700: #fde68a; + --color-warning-600: #fcd34d; + --color-warning-500: #fbbf24; + --color-warning-400: #f59e0b; + --color-warning-300: #d97706; + --color-warning-200: #b45309; + --color-warning-100: #92400e; + --color-warning-50: #783900; + --color-warning-000: #411e01; + + --color-positive-900: #f0fdf4; + --color-positive-800: #dcfce7; + --color-positive-700: #bbf7d0; + --color-positive-600: #86efac; + --color-positive-500: #4ade80; + --color-positive-400: #22c55e; + --color-positive-300: #16a34a; + --color-positive-200: #15803d; + --color-positive-100: #166534; + --color-positive-50: #14532d; + --color-positive-000: #0f2e1b; + + --color-urge-900: #e0ffff; + --color-urge-800: #c0fbff; + --color-urge-700: #a0f4ff; + --color-urge-600: #80e9ff; + --color-urge-500: #60dbfb; + --color-urge-400: #40cbf3; + --color-urge-300: #20b9e9; + --color-urge-200: #00a4dc; /* tab buttons */ + --color-urge-100: #0054bc; + --color-urge-50: #00169a; + --color-urge-000: #050076; + + --color-info-900: #f5f3ff; + --color-info-800: #ede9fe; + --color-info-700: #ddd6fe; + --color-info-600: #c4b5fd; + --color-info-500: #a78bfa; + --color-info-400: #8b5cf6; + --color-info-300: #7c3aed; + --color-info-200: #6d28d9; + --color-info-100: #5b21b6; + --color-info-50: #4c1d95; + --color-info-000: #240e44; + + + --color-neutral-normal-content: #ffffff; +} + +.light-only { + display: none; +} + +.dark-only { + display: initial; +} diff --git a/css/loader.css b/css/loader.css new file mode 100644 index 0000000..2b7951a --- /dev/null +++ b/css/loader.css @@ -0,0 +1,40 @@ +.loader { + height: auto; + color: rgba(0, 0, 0, 0); +} + +.loader .dot { + --diameter: 0.5rem; + --radius: calc(var(--diameter) / 2); + --deviation: 20%; + height: var(--diameter); + width: var(--diameter); + background-color: var(--color-content); + border-radius: 50%; + position: absolute; + left: calc(50% - var(--radius)); + animation: osc 1s cubic-bezier(.72,.16,.31,.97) infinite; +} +.loader.loader-sm .dot { + --deviation: 10%; +} + +@keyframes osc { + 25% { + left: calc(50% + var(--deviation) - var(--radius)); + } + 75% { + left: calc(50% - var(--deviation)); + } + 0%, 50%, 100% { + left: calc(50% - var(--radius)); + } +/* + 0%, 100% { + left: calc(50% - var(--deviation)) + } + 50% { + left: calc(50% + var(--deviation) - var(--radius)); + } + */ +} diff --git a/css/modal.css b/css/modal.css new file mode 100644 index 0000000..3f27a99 --- /dev/null +++ b/css/modal.css @@ -0,0 +1,72 @@ +.modal { + display: none; + position: fixed; + z-index: 12; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,40%); +} + +.modal-shown { + display: block; +} + +@keyframes modal-hide { + from { opacity: 1; } + to { opacity: 0; } +} + +.modal-hiding { + animation: modal-hide 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +@keyframes modal-content-show { + from { + opacity: 0; + top: -6rem; + } + to { + opacity: 1; + top: 0; + } +} + +.modal-content { + position: relative; + margin: 10% auto; + width: 30%; +} + +.modal-content.wide { + width: 60%; +} + +.modal-shown .modal-content { + animation: modal-content-show 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +@media screen and (max-width: 1000px) { + .modal-content.wide { + width: 75%; + } +} + +@media screen and (max-width: 400px) { + .modal-content, .modal-content.wide { + width: 90%; + } +} + +.modal-close { + float: right; + color: #aaa; + font-weight: normal; +} + +.modal-close:hover, +.modal-close:focus { + filter: brightness(60%); +} diff --git a/css/tooltip.css b/css/tooltip.css new file mode 100644 index 0000000..4566f30 --- /dev/null +++ b/css/tooltip.css @@ -0,0 +1,36 @@ +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip .content { + visibility: hidden; + max-width: 10rem; + min-width: 6rem; + background-color: rgba(0, 0, 0, 0.6); + color: #fff; + padding: 0.5rem; + border-radius: 6px; + overflow-wrap: break-word; + text-align: center; + + position: absolute; + z-index: 1; + top: -1rem; +} + +.tooltip.right .content { + left: 120%; +} + +.tooltip.left .content { + right: 120%; +} + +.tooltip .content.sm { + font-size: 0.8rem; +} + +.tooltip:hover .content { + visibility: visible; +} diff --git a/data/static/setup.js b/data/static/setup.js deleted file mode 100644 index e9f5fc6..0000000 --- a/data/static/setup.js +++ /dev/null @@ -1,275 +0,0 @@ -document.getElementById('page-1').scrollIntoView({ - behavior: 'auto', - block: 'center', - inline: 'center' }); - -function checkAuthRadio() { - if (document.getElementById('manualAuthRadio').checked) { - document.getElementById('adminOnlyArea').style.display = 'none'; - document.getElementById('manualAuthArea').style.display = ''; - } else { - document.getElementById('manualAuthArea').style.display = 'none'; - document.getElementById('adminOnlyArea').style.display = ''; - }; -}; -var authRadios = ['manualAuthRadio', 'jfAuthRadio']; -for (var i = 0; i < authRadios.length; i++) { - document.getElementById(authRadios[i]).addEventListener('change', function() { - checkAuthRadio(); - }); -}; - -function checkEmailRadio() { - document.getElementById('emailNextButton').href = '#page-5'; - document.getElementById('valBackButton').href = '#page-7'; - if (document.getElementById('emailSMTPRadio').checked) { - document.getElementById('emailCommonArea').style.display = ''; - document.getElementById('emailSMTPArea').style.display = ''; - document.getElementById('emailMailgunArea').style.display = 'none'; - document.getElementById('notificationsEnabled').checked = true; - } else if (document.getElementById('emailMailgunRadio').checked) { - document.getElementById('emailCommonArea').style.display = ''; - document.getElementById('emailSMTPArea').style.display = 'none'; - document.getElementById('emailMailgunArea').style.display = ''; - document.getElementById('notificationsEnabled').checked = true; - } else if (document.getElementById('emailDisabledRadio').checked) { - document.getElementById('emailCommonArea').style.display = 'none'; - document.getElementById('emailSMTPArea').style.display = 'none'; - document.getElementById('emailMailgunArea').style.display = 'none'; - document.getElementById('emailNextButton').href = '#page-8'; - document.getElementById('valBackButton').href = '#page-4'; - document.getElementById('notificationsEnabled').checked = false; - }; -}; -var emailRadios = ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio']; -for (var i = 0; i < emailRadios.length; i++) { - document.getElementById(emailRadios[i]).addEventListener('change', function() { - checkEmailRadio(); - }); -}; - -function checkSSL() { - var label = document.getElementById('emailSSL_TLSLabel'); - if (document.getElementById('emailSSL_TLS').checked) { - label.textContent = 'Use SSL/TLS'; - } else { - label.textContent = 'Use STARTTLS'; - }; -}; -document.getElementById('emailSSL_TLS').addEventListener('change', function() { - checkSSL(); -}); - -function checkPwrEnabled() { - if (document.getElementById('pwrEnabled').checked) { - document.getElementById('pwrArea').style.display = ''; - } else { - document.getElementById('pwrArea').style.display = 'none'; - }; -}; -var pwrEnabled = document.getElementById('pwrEnabled'); -pwrEnabled.addEventListener('change', function() { - checkPwrEnabled(); -}); - -function checkInvEnabled() { - if (document.getElementById('invEnabled').checked) { - document.getElementById('invArea').style.display = ''; - } else { - document.getElementById('invArea').style.display = 'none'; - }; -}; -document.getElementById('invEnabled').addEventListener('change', function() { - checkInvEnabled(); -}); - -function checkValEnabled() { - if (document.getElementById('valEnabled').checked) { - document.getElementById('valArea').style.display = ''; - } else { - document.getElementById('valArea').style.display = 'none'; - }; -}; -document.getElementById('valEnabled').addEventListener('change', function() { - checkValEnabled(); -}); -checkValEnabled(); -checkInvEnabled(); -checkSSL(); -checkAuthRadio(); -checkEmailRadio(); -checkPwrEnabled(); - -var jfValid = false -document.getElementById('jfTestButton').onclick = function() { - var testButton = document.getElementById('jfTestButton'); - var nextButton = document.getElementById('jfNextButton'); - var jfData = {}; - jfData['jfHost'] = document.getElementById('jfHost').value; - jfData['jfUser'] = document.getElementById('jfUser').value; - jfData['jfPassword'] = document.getElementById('jfPassword').value; - let valid = true; - for (val in jfData) { - if (jfData[val] == "") { - valid = false; - } - } - if (!valid) { - if (!testButton.classList.contains('btn-danger')) { - testButton.classList.add('btn-danger'); - testButton.textContent = 'Fill out fields above.'; - setTimeout(function() { - if (testButton.classList.contains('btn-danger')) { - testButton.classList.remove('btn-danger'); - testButton.textContent = 'Test'; - } - }, 2000); - } - } else { - testButton.disabled = true; - testButton.innerHTML = - '' + - 'Testing...'; - nextButton.classList.add('disabled'); - nextButton.setAttribute('aria-disabled', 'true'); - var req = new XMLHttpRequest(); - req.open("POST", "/jellyfin/test", true); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.responseType = 'json'; - req.onreadystatechange = function() { - if (this.readyState == 4) { - testButton.disabled = false; - testButton.className = ''; - if (this.response['success'] == true) { - testButton.classList.add('btn', 'btn-success'); - testButton.textContent = 'Success'; - nextButton.classList.remove('disabled'); - nextButton.setAttribute('aria-disabled', 'false'); - } else { - testButton.classList.add('btn', 'btn-danger'); - testButton.textContent = 'Failed'; - }; - }; - }; - req.send(JSON.stringify(jfData)); - } -}; - -document.getElementById('submitButton').onclick = function() { - var submitButton = document.getElementById('submitButton'); - submitButton.disabled = true; - submitButton.innerHTML = - '' + - 'Submitting...'; - var config = {}; - config['jellyfin'] = {}; - config['ui'] = {}; - config['password_validation'] = {}; - config['email'] = {}; - config['password_resets'] = {}; - config['invite_emails'] = {}; - config['mailgun'] = {}; - config['smtp'] = {}; - config['notifications'] = {}; - // Page 2: Auth - if (document.getElementById('jfAuthRadio').checked) { - config['ui']['jellyfin_login'] = 'true'; - if (document.getElementById('jfAuthAdminOnly').checked) { - config['ui']['admin_only'] = 'true'; - } else { - config['ui']['admin_only'] = 'false' - }; - } else { - config['ui']['username'] = document.getElementById('manualAuthUsername').value; - config['ui']['password'] = document.getElementById('manualAuthPassword').value; - config['ui']['email'] = document.getElementById('manualAuthEmail').value; - }; - // Page 3: Connect to jellyfin - config['jellyfin']['server'] = document.getElementById('jfHost').value; - let publicAddress = document.getElementById('jfPublicHost').value; - if (publicAddress != "") { - config['jellyfin']['public_server'] = publicAddress; - } - config['jellyfin']['username'] = document.getElementById('jfUser').value; - config['jellyfin']['password'] = document.getElementById('jfPassword').value; - // Page 4: Email (Page 5, 6, 7 are only used if this is enabled) - if (document.getElementById('emailDisabledRadio').checked) { - config['password_resets']['enabled'] = 'false'; - config['invite_emails']['enabled'] = 'false'; - config['notifications']['enabled'] = 'false'; - } else { - if (document.getElementById('emailSMTPRadio').checked) { - if (document.getElementById('emailSSL_TLS').checked) { - config['smtp']['encryption'] = 'ssl_tls'; - } else { - config['smtp']['encryption'] = 'starttls'; - }; - config['email']['method'] = 'smtp'; - config['smtp']['server'] = document.getElementById('emailSMTPServer').value; - config['smtp']['port'] = document.getElementById('emailSMTPPort').value; - config['smtp']['password'] = document.getElementById('emailSMTPPassword').value; - config['email']['address'] = document.getElementById('emailSMTPAddress').value; - } else { - config['email']['method'] = 'mailgun'; - config['mailgun']['api_url'] = document.getElementById('emailMailgunURL').value; - config['mailgun']['api_key'] = document.getElementById('emailMailgunKey').value; - config['email']['address'] = document.getElementById('emailMailgunAddress').value; - }; - config['notifications']['enabled'] = document.getElementById('notificationsEnabled').checked.toString(); - // Page 5: Email formatting - config['email']['from'] = document.getElementById('emailSender').value; - config['email']['date_format'] = document.getElementById('emailDateFormat').value; - if (document.getElementById('email24hTimeRadio').checked) { - config['email']['use_24h'] = 'true'; - } else { - config['email']['use_24h'] = 'false'; - }; - config['email']['message'] = document.getElementById('emailMessage').value; - // Page 6: Password Resets - if (document.getElementById('pwrEnabled').checked) { - config['password_resets']['enabled'] = 'true'; - config['password_resets']['watch_directory'] = document.getElementById('pwrJfPath').value; - config['password_resets']['subject'] = document.getElementById('pwrSubject').value; - } else { - config['password_resets']['enabled'] = 'false'; - }; - // Page 7: Invite Emails - if (document.getElementById('invEnabled').checked) { - config['invite_emails']['enabled'] = 'true'; - config['invite_emails']['url_base'] = document.getElementById('invURLBase').value; - config['invite_emails']['subject'] = document.getElementById('invSubject').value; - } else { - config['invite_emails']['enabled'] = 'false'; - }; - }; - // Page 8: Password Validation - if (document.getElementById('valEnabled').checked) { - config['password_validation']['enabled'] = 'true'; - config['password_validation']['min_length'] = document.getElementById('valLength').value; - config['password_validation']['upper'] = document.getElementById('valUpper').value; - config['password_validation']['lower'] = document.getElementById('valLower').value; - config['password_validation']['number'] = document.getElementById('valNumber').value; - config['password_validation']['special'] = document.getElementById('valSpecial').value; - } else { - config['password_validation']['enabled'] = 'false'; - }; - // Page 9: Messages - config['ui']['contact_message'] = document.getElementById('msgContact').value; - config['ui']['help_message'] = document.getElementById('msgHelp').value; - config['ui']['success_message'] = document.getElementById('msgSuccess').value; - // Send it - config["restart-program"] = true; - var req = new XMLHttpRequest(); - req.open("POST", "/config", true); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.responseType = 'json'; - req.onreadystatechange = function() { - if (this.readyState == 4) { - submitButton.disabled = false; - submitButton.className = ''; - submitButton.classList.add('btn', 'btn-success'); - submitButton.textContent = 'Success'; - }; - }; - req.send(JSON.stringify(config)); -}; diff --git a/data/templates/404.html b/data/templates/404.html deleted file mode 100644 index 22593ad..0000000 --- a/data/templates/404.html +++ /dev/null @@ -1,21 +0,0 @@ - - - -- {{ else }} - | - {{ end }} - | Username | -Email Address | -Last Active | -
---|
Note: * Indicates required field, R Indicates changes require a restart.
- - - {{ if .ombiEnabled }} - - {{ end }} -Profiles are applied to users when they create an account. They include things like access rights and homescreen layout. You can create them here.
-Name | -Default | -From | -Admin? | -Libraries | -- |
---|
{{ .contactMessage }}
-{{ .helpMessage }}
-{{ .contactMessage }}
-+ {{ .contactMessage }} +
+Warning invites with infinite uses can be used abusively.
+ ++ | Username | +Email Address | +Last Active | +
---|
The code above was either incorrect, or has expired.
++ {{ .contactMessage }} +
+