From 9dbf60e3dfc9657aa0d84ebb164d384783939381 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 22 Nov 2020 16:36:43 +0000 Subject: [PATCH] add URL base option for subfolder proxies also cleaned up the naming of some things. --- api.go | 65 +++++++++++++------------ config.go | 29 +++++------ config/config-base.json | 8 ++++ daemon.go | 10 ++-- data/templates/admin.html | 11 +++-- data/templates/form-base.html | 1 + data/templates/form.html | 10 ++-- email.go | 2 +- main.go | 90 ++++++++++++++++++----------------- pwreset.go | 5 +- pwval.go | 1 + ts/admin.ts | 6 ++- ts/modules/common.ts | 6 +-- ts/typings/d.ts | 1 + views.go | 15 ++++-- 15 files changed, 145 insertions(+), 115 deletions(-) diff --git a/api.go b/api.go index 66e6896..f8067a4 100644 --- a/api.go +++ b/api.go @@ -105,12 +105,12 @@ func timeDiff(a, b time.Time) (year, month, day, hour, min, sec int) { } func (app *appContext) checkInvites() { - current_time := time.Now() + currentTime := time.Now() app.storage.loadInvites() changed := false for code, data := range app.storage.invites { expiry := data.ValidTill - if !current_time.After(expiry) { + if !currentTime.After(expiry) { continue } app.debug.Printf("Housekeeping: Deleting old invite %s", code) @@ -144,7 +144,7 @@ func (app *appContext) checkInvites() { } func (app *appContext) checkInvite(code string, used bool, username string) bool { - current_time := time.Now() + currentTime := time.Now() app.storage.loadInvites() changed := false inv, match := app.storage.invites[code] @@ -152,7 +152,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool return false } expiry := inv.ValidTill - if current_time.After(expiry) { + if currentTime.After(expiry) { app.debug.Printf("Housekeeping: Deleting old invite %s", code) notify := inv.Notify if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { @@ -188,7 +188,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool // 0 means infinite i guess? newInv.RemainingUses -= 1 } - newInv.UsedBy = append(newInv.UsedBy, []string{username, app.formatDatetime(current_time)}) + newInv.UsedBy = append(newInv.UsedBy, []string{username, app.formatDatetime(currentTime)}) if !del { app.storage.invites[code] = newInv } @@ -256,15 +256,17 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { } if len(app.storage.policy) != 0 { status, err = app.jf.SetPolicy(id, app.storage.policy) - if !(status == 200 || status == 204) { + if !(status == 200 || status == 204 || err == nil) { app.err.Printf("%s: Failed to set user policy: Code %d", req.Username, status) + app.debug.Printf("%s: Error: %s", req.Username, err) } } if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 { status, err = app.jf.SetConfiguration(id, app.storage.configuration) if (status == 200 || status == 204) && err == nil { status, err = app.jf.SetDisplayPreferences(id, app.storage.displayprefs) - } else { + } + if !((status == 200 || status == 204) && err == nil) { app.err.Printf("%s: Failed to set configuration template: Code %d", req.Username, status) } } @@ -364,8 +366,9 @@ func (app *appContext) NewUser(gc *gin.Context) { if len(profile.Policy) != 0 { app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) status, err = app.jf.SetPolicy(id, profile.Policy) - if !(status == 200 || status == 204) { + if !((status == 200 || status == 204) && err == nil) { app.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status) + app.debug.Printf("%s: Error: %s", req.Code, err) } } if len(profile.Configuration) != 0 && len(profile.Displayprefs) != 0 { @@ -373,8 +376,10 @@ func (app *appContext) NewUser(gc *gin.Context) { status, err = app.jf.SetConfiguration(id, profile.Configuration) if (status == 200 || status == 204) && err == nil { status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) - } else { + } + if !((status == 200 || status == 204) && err == nil) { app.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status) + app.debug.Printf("%s: Error: %s", req.Code, err) } } } @@ -482,18 +487,18 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { app.debug.Println("Generating new invite") app.storage.loadInvites() gc.BindJSON(&req) - current_time := time.Now() - valid_till := current_time.AddDate(0, 0, req.Days) - valid_till = valid_till.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes)) + currentTime := time.Now() + validTill := currentTime.AddDate(0, 0, req.Days) + validTill = validTill.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes)) // make sure code doesn't begin with number - invite_code := shortuuid.New() - _, err := strconv.Atoi(string(invite_code[0])) + inviteCode := shortuuid.New() + _, err := strconv.Atoi(string(inviteCode[0])) for err == nil { - invite_code = shortuuid.New() - _, err = strconv.Atoi(string(invite_code[0])) + inviteCode = shortuuid.New() + _, err = strconv.Atoi(string(inviteCode[0])) } var invite Invite - invite.Created = current_time + invite.Created = currentTime if req.MultipleUses { if req.NoLimit { invite.NoLimit = true @@ -503,21 +508,21 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { } else { invite.RemainingUses = 1 } - invite.ValidTill = valid_till + invite.ValidTill = validTill if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { - app.debug.Printf("%s: Sending invite email", invite_code) + app.debug.Printf("%s: Sending invite email", inviteCode) invite.Email = req.Email - msg, err := app.email.constructInvite(invite_code, invite, app) + msg, err := app.email.constructInvite(inviteCode, invite, app) if err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) - app.err.Printf("%s: Failed to construct invite email", invite_code) - app.debug.Printf("%s: Error: %s", invite_code, err) + app.err.Printf("%s: Failed to construct invite email", inviteCode) + app.debug.Printf("%s: Error: %s", inviteCode, err) } else if err := app.email.send(req.Email, msg); err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) - app.err.Printf("%s: %s", invite_code, invite.Email) - app.debug.Printf("%s: Error: %s", invite_code, err) + app.err.Printf("%s: %s", inviteCode, invite.Email) + app.debug.Printf("%s: Error: %s", inviteCode, err) } else { - app.info.Printf("%s: Sent invite email to %s", invite_code, req.Email) + app.info.Printf("%s: Sent invite email to %s", inviteCode, req.Email) } } if req.Profile != "" { @@ -527,7 +532,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { invite.Profile = "Default" } } - app.storage.invites[invite_code] = invite + app.storage.invites[inviteCode] = invite app.storage.storeInvites() respondBool(200, true, gc) } @@ -972,7 +977,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) { // @Produce json // @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings" // @Success 200 {object} errorListDTO -// @Failure 500 {object} errorListDTO "Lists of errors that occured while applying settings" +// @Failure 500 {object} errorListDTO "Lists of errors that occurred while applying settings" // @Router /users/settings [post] // @Security Bearer // @tags Profiles & Settings @@ -1063,7 +1068,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) { func (app *appContext) GetConfig(gc *gin.Context) { app.info.Println("Config requested") resp := map[string]interface{}{} - langPath := filepath.Join(app.local_path, "lang", "form") + langPath := filepath.Join(app.localPath, "lang", "form") app.lang.langFiles, _ = ioutil.ReadDir(langPath) app.lang.langOptions = make([]string, len(app.lang.langFiles)) chosenLang := app.config.Section("ui").Key("language").MustString("en-us") + ".json" @@ -1124,7 +1129,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { app.info.Println("Config modification requested") var req configDTO gc.BindJSON(&req) - tempConfig, _ := ini.Load(app.config_path) + tempConfig, _ := ini.Load(app.configPath) for section, settings := range req { if section != "restart-program" { _, err := tempConfig.GetSection(section) @@ -1145,7 +1150,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { } } } - tempConfig.SaveTo(app.config_path) + tempConfig.SaveTo(app.configPath) app.debug.Println("Config saved") gc.JSON(200, map[string]bool{"success": true}) if req["restart-program"] != nil && req["restart-program"].(bool) { diff --git a/config.go b/config.go index 93f3acd..71e1724 100644 --- a/config.go +++ b/config.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "strconv" + "strings" "gopkg.in/ini.v1" ) @@ -36,7 +37,7 @@ func (app *appContext) loadDefaults() (err error) { func (app *appContext) loadConfig() error { var err error - app.config, err = ini.Load(app.config_path) + app.config, err = ini.Load(app.configPath) if err != nil { return err } @@ -48,32 +49,32 @@ func (app *appContext) loadConfig() error { // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) // } if key.Name() != "html_templates" { - key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json")))) + key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) } } for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template"} { // if app.config.Section("files").Key(key).MustString("") == "" { // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) // } - app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.data_path, (key + ".json")))) + app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json")))) } - + app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/") app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false))) - app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString(filepath.Join(app.local_path, "email.html"))) - app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString(filepath.Join(app.local_path, "email.txt"))) + app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString(filepath.Join(app.localPath, "email.html"))) + app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString(filepath.Join(app.localPath, "email.txt"))) - app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(app.local_path, "invite-email.html"))) - app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(app.local_path, "invite-email.txt"))) + app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(app.localPath, "invite-email.html"))) + app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(app.localPath, "invite-email.txt"))) - app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.local_path, "expired.html"))) - app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.local_path, "expired.txt"))) + app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.localPath, "expired.html"))) + app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.localPath, "expired.txt"))) - app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.local_path, "created.html"))) - app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.local_path, "created.txt"))) + app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.localPath, "created.html"))) + app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.localPath, "created.txt"))) - app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString(filepath.Join(app.local_path, "deleted.html"))) - app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString(filepath.Join(app.local_path, "deleted.txt"))) + app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString(filepath.Join(app.localPath, "deleted.html"))) + app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString(filepath.Join(app.localPath, "deleted.txt"))) app.config.Section("jellyfin").Key("version").SetValue(VERSION) app.config.Section("jellyfin").Key("device").SetValue("jfa-go") diff --git a/config/config-base.json b/config/config-base.json index 67508a0..21e95a2 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -179,6 +179,14 @@ "type": "bool", "value": false, "description": "Use the Bootstrap 5 Alpha. Looks better and removes the need for jQuery, so the page should load faster." + }, + "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": { diff --git a/daemon.go b/daemon.go index a8827e1..51299dc 100644 --- a/daemon.go +++ b/daemon.go @@ -4,7 +4,7 @@ import "time" // https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS -type Repeater struct { +type repeater struct { Stopped bool ShutdownChannel chan string Interval time.Duration @@ -12,8 +12,8 @@ type Repeater struct { app *appContext } -func NewRepeater(interval time.Duration, app *appContext) *Repeater { - return &Repeater{ +func newRepeater(interval time.Duration, app *appContext) *repeater { + return &repeater{ Stopped: false, ShutdownChannel: make(chan string), Interval: interval, @@ -22,7 +22,7 @@ func NewRepeater(interval time.Duration, app *appContext) *Repeater { } } -func (rt *Repeater) Run() { +func (rt *repeater) run() { rt.app.info.Println("Invite daemon started") for { select { @@ -42,7 +42,7 @@ func (rt *Repeater) Run() { } } -func (rt *Repeater) Shutdown() { +func (rt *repeater) shutdown() { rt.Stopped = true rt.ShutdownChannel <- "Down" <-rt.ShutdownChannel diff --git a/data/templates/admin.html b/data/templates/admin.html index d40b4f6..3e8b5fb 100644 --- a/data/templates/admin.html +++ b/data/templates/admin.html @@ -5,11 +5,11 @@ - - - - - + + + + + @@ -30,6 +30,7 @@ } return ""; } + window.URLBase = "{{ .urlBase }}"; {{ if .bs5 }} window.bsVersion = 5; {{ else }} diff --git a/data/templates/form-base.html b/data/templates/form-base.html index 1c8bbf0..5f1aa62 100644 --- a/data/templates/form-base.html +++ b/data/templates/form-base.html @@ -4,6 +4,7 @@ window.usernameEnabled = {{ .settings.username }}; window.validationStrings = JSON.parse({{ .lang.validationStrings }}); window.invalidPassword = "{{ .lang.reEnterPasswordInvalid }}"; + window.URLBase = "{{ .urlBase }}"; {{ end }} diff --git a/data/templates/form.html b/data/templates/form.html index cd16cc6..6982736 100644 --- a/data/templates/form.html +++ b/data/templates/form.html @@ -4,11 +4,11 @@ - - - - - + + + + + diff --git a/email.go b/email.go index 45c7b16..10bd6c0 100644 --- a/email.go +++ b/email.go @@ -252,7 +252,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite return email, nil } -func (emailer *Emailer) constructReset(pwr Pwr, app *appContext) (*Email, error) { +func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Email, error) { email := &Email{ subject: app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin"), } diff --git a/main.go b/main.go index f53c1ad..78da16a 100644 --- a/main.go +++ b/main.go @@ -34,7 +34,7 @@ import ( "gopkg.in/ini.v1" ) -// Username is JWT! +// User is used for auth purposes. type User struct { UserID string `json:"id"` Username string `json:"username"` @@ -44,11 +44,11 @@ type User struct { type appContext struct { // defaults *Config config *ini.File - config_path string - configBase_path string + configPath string + configBasePath string configBase map[string]interface{} - data_path string - local_path string + dataPath string + localPath string cssFile string bsVersion int jellyfinLogin bool @@ -68,8 +68,10 @@ type appContext struct { version string quit chan os.Signal lang Languages + URLBase string } +// Languages stores the names and filenames of language files, and the index of that which is currently selected. type Languages struct { langFiles []os.FileInfo // Language filenames langOptions []string // Language names @@ -78,10 +80,10 @@ type Languages struct { func (app *appContext) loadHTML(router *gin.Engine) { customPath := app.config.Section("files").Key("html_templates").MustString("") - templatePath := filepath.Join(app.local_path, "templates") + templatePath := filepath.Join(app.localPath, "templates") htmlFiles, err := ioutil.ReadDir(templatePath) if err != nil { - app.err.Fatalf("Couldn't access template directory: \"%s\"", filepath.Join(app.local_path, "templates")) + app.err.Fatalf("Couldn't access template directory: \"%s\"", filepath.Join(app.localPath, "templates")) return } loadFiles := make([]string, len(htmlFiles)) @@ -97,7 +99,7 @@ func (app *appContext) loadHTML(router *gin.Engine) { router.LoadHTMLFiles(loadFiles...) } -func GenerateSecret(length int) (string, error) { +func generateSecret(length int) (string, error) { bytes := make([]byte, length) _, err := rand.Read(bytes) if err != nil { @@ -173,7 +175,7 @@ func test(app *appContext) { fmt.Scanln(&username) user, status, err := app.jf.UserByName(username, false) fmt.Printf("UserByName (%s): code %d err %s", username, status, err) - out, err := json.MarshalIndent(user, "", " ") + out, _ := json.MarshalIndent(user, "", " ") fmt.Print(string(out)) } @@ -187,17 +189,17 @@ func start(asDaemon, firstCall bool) { local_path is the internal 'data' directory. */ userConfigDir, _ := os.UserConfigDir() - app.data_path = filepath.Join(userConfigDir, "jfa-go") - app.config_path = filepath.Join(app.data_path, "config.ini") + app.dataPath = filepath.Join(userConfigDir, "jfa-go") + app.configPath = filepath.Join(app.dataPath, "config.ini") executable, _ := os.Executable() - app.local_path = filepath.Join(filepath.Dir(executable), "data") + app.localPath = filepath.Join(filepath.Dir(executable), "data") app.info = log.New(os.Stdout, "[INFO] ", log.Ltime) app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile) if firstCall { - DATA = flag.String("data", app.data_path, "alternate path to data directory.") - CONFIG = flag.String("config", app.config_path, "alternate path to config file.") + DATA = flag.String("data", app.dataPath, "alternate path to data directory.") + CONFIG = flag.String("config", app.configPath, "alternate path to config file.") HOST = flag.String("host", "", "alternate address to host web ui on.") PORT = flag.Int("port", 0, "alternate port to host web ui on.") DEBUG = flag.Bool("debug", false, "Enables debug logging and exposes pprof.") @@ -219,35 +221,35 @@ func start(asDaemon, firstCall bool) { *DEBUG = true } // attempt to apply command line flags correctly - if app.config_path == *CONFIG && app.data_path != *DATA { - app.data_path = *DATA - app.config_path = filepath.Join(app.data_path, "config.ini") - } else if app.config_path != *CONFIG && app.data_path == *DATA { - app.config_path = *CONFIG + if app.configPath == *CONFIG && app.dataPath != *DATA { + app.dataPath = *DATA + app.configPath = filepath.Join(app.dataPath, "config.ini") + } else if app.configPath != *CONFIG && app.dataPath == *DATA { + app.configPath = *CONFIG } else { - app.config_path = *CONFIG - app.data_path = *DATA + app.configPath = *CONFIG + app.dataPath = *DATA } // env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason. if v := os.Getenv("JFA_CONFIGPATH"); v != "" { - app.config_path = v + app.configPath = v } if v := os.Getenv("JFA_DATAPATH"); v != "" { - app.data_path = v + app.dataPath = v } - os.Setenv("JFA_CONFIGPATH", app.config_path) - os.Setenv("JFA_DATAPATH", app.data_path) + os.Setenv("JFA_CONFIGPATH", app.configPath) + os.Setenv("JFA_DATAPATH", app.dataPath) var firstRun bool - if _, err := os.Stat(app.data_path); os.IsNotExist(err) { - os.Mkdir(app.data_path, 0700) + if _, err := os.Stat(app.dataPath); os.IsNotExist(err) { + os.Mkdir(app.dataPath, 0700) } - if _, err := os.Stat(app.config_path); os.IsNotExist(err) { + if _, err := os.Stat(app.configPath); os.IsNotExist(err) { firstRun = true - dConfigPath := filepath.Join(app.local_path, "config-default.ini") + dConfigPath := filepath.Join(app.localPath, "config-default.ini") var dConfig *os.File dConfig, err = os.Open(dConfigPath) if err != nil { @@ -255,28 +257,28 @@ func start(asDaemon, firstCall bool) { } defer dConfig.Close() var nConfig *os.File - nConfig, err := os.Create(app.config_path) + nConfig, err := os.Create(app.configPath) if err != nil { - app.err.Printf("Couldn't open config file for writing: \"%s\"", app.config_path) + app.err.Printf("Couldn't open config file for writing: \"%s\"", app.configPath) app.err.Fatalf("Error: %s", err) } defer nConfig.Close() _, err = io.Copy(nConfig, dConfig) if err != nil { - app.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, app.config_path) + app.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, app.configPath) } - app.info.Printf("Copied default configuration to \"%s\"", app.config_path) + app.info.Printf("Copied default configuration to \"%s\"", app.configPath) } var debugMode bool var address string if app.loadConfig() != nil { - app.err.Fatalf("Failed to load config file \"%s\"", app.config_path) + app.err.Fatalf("Failed to load config file \"%s\"", app.configPath) } lang := app.config.Section("ui").Key("language").MustString("en-us") - app.storage.lang.FormPath = filepath.Join(app.local_path, "lang", "form", lang+".json") + app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form", lang+".json") if _, err := os.Stat(app.storage.lang.FormPath); os.IsNotExist(err) { - app.storage.lang.FormPath = filepath.Join(app.local_path, "lang", "form", "en-us.json") + app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form", "en-us.json") } app.storage.loadLang() app.version = app.config.Section("jellyfin").Key("version").String() @@ -356,7 +358,7 @@ func start(asDaemon, firstCall bool) { address = fmt.Sprintf("%s:%d", app.host, app.port) - app.debug.Printf("Loaded config file \"%s\"", app.config_path) + app.debug.Printf("Loaded config file \"%s\"", app.configPath) if app.config.Section("ui").Key("bs5").MustBool(false) { app.cssFile = "bs5-jf.css" @@ -411,8 +413,8 @@ func start(asDaemon, firstCall bool) { } - app.configBase_path = filepath.Join(app.local_path, "config-base.json") - configBase, _ := ioutil.ReadFile(app.configBase_path) + app.configBasePath = filepath.Join(app.localPath, "config-base.json") + configBase, _ := ioutil.ReadFile(app.configBasePath) json.Unmarshal(configBase, &app.configBase) themes := map[string]string{ @@ -424,7 +426,7 @@ func start(asDaemon, firstCall bool) { app.cssFile = val } app.debug.Printf("Using css file \"%s\"", app.cssFile) - secret, err := GenerateSecret(16) + secret, err := generateSecret(16) if err != nil { app.err.Fatal(err) } @@ -481,8 +483,8 @@ func start(asDaemon, firstCall bool) { os.Exit(0) } - inviteDaemon := NewRepeater(time.Duration(60*time.Second), app) - go inviteDaemon.Run() + inviteDaemon := newRepeater(time.Duration(60*time.Second), app) + go inviteDaemon.run() if app.config.Section("password_resets").Key("enabled").MustBool(false) { go app.StartPWR() @@ -502,7 +504,7 @@ func start(asDaemon, firstCall bool) { setGinLogger(router, debugMode) router.Use(gin.Recovery()) - router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.local_path, "static"), false))) + router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.localPath, "static"), false))) app.loadHTML(router) router.NoRoute(app.NoRouteHandler) if debugMode { @@ -514,7 +516,7 @@ func start(asDaemon, firstCall bool) { router.GET("/token/login", app.getTokenLogin) router.GET("/token/refresh", app.getTokenRefresh) router.POST("/newUser", app.NewUser) - router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.local_path, "static"), false))) + router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.localPath, "static"), false))) router.GET("/invite/:invCode", app.InviteProxy) if *SWAGGER { app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) diff --git a/pwreset.go b/pwreset.go index 2ca7e1a..7e91a47 100644 --- a/pwreset.go +++ b/pwreset.go @@ -34,7 +34,8 @@ func (app *appContext) StartPWR() { <-done } -type Pwr struct { +// PasswordReset represents a passwordreset-xyz.json file generated by Jellyfin. +type PasswordReset struct { Pin string `json:"Pin"` Username string `json:"UserName"` Expiry time.Time `json:"ExpirationDate"` @@ -48,7 +49,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { return } if event.Op&fsnotify.Write == fsnotify.Write && strings.Contains(event.Name, "passwordreset") { - var pwr Pwr + var pwr PasswordReset data, err := ioutil.ReadFile(event.Name) if err != nil { return diff --git a/pwval.go b/pwval.go index 8699e7e..96dcbc3 100644 --- a/pwval.go +++ b/pwval.go @@ -4,6 +4,7 @@ import ( "unicode" ) +// Validator allows for validation of passwords. type Validator struct { minLength, upper, lower, number, special int criteria ValidatorConf diff --git a/ts/admin.ts b/ts/admin.ts index c920173..33eb546 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -121,10 +121,12 @@ window.toClipboard = (str: string): void => { function login(username: string, password: string, modal: boolean, button?: HTMLButtonElement, run?: (arg0: number) => void): void { const req = new XMLHttpRequest(); req.responseType = 'json'; - let url = "/token/login"; + let url = window.URLBase; const refresh = (username == "" && password == ""); if (refresh) { - url = "/token/refresh"; + url += "/token/refresh"; + } else { + url += "/token/login"; } req.open("GET", url, true); if (!refresh) { diff --git a/ts/modules/common.ts b/ts/modules/common.ts index 209cb19..c6b25be 100644 --- a/ts/modules/common.ts +++ b/ts/modules/common.ts @@ -46,7 +46,7 @@ export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add export const _get = (url: string, data: Object, onreadystatechange: () => void): void => { let req = new XMLHttpRequest(); - req.open("GET", url, true); + req.open("GET", window.URLBase + url, true); req.responseType = 'json'; req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); @@ -56,7 +56,7 @@ export const _get = (url: string, data: Object, onreadystatechange: () => void): export const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => { let req = new XMLHttpRequest(); - req.open("POST", url, true); + req.open("POST", window.URLBase + url, true); if (response) { req.responseType = 'json'; } @@ -68,7 +68,7 @@ export const _post = (url: string, data: Object, onreadystatechange: () => void, export function _delete(url: string, data: Object, onreadystatechange: () => void): void { let req = new XMLHttpRequest(); - req.open("DELETE", url, true); + req.open("DELETE", window.URLBase + url, true); req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); req.onreadystatechange = onreadystatechange; diff --git a/ts/typings/d.ts b/ts/typings/d.ts index 7bc6c51..5be1863 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -14,6 +14,7 @@ declare interface Window { bsVersion: number; bs5: boolean; BS: Bootstrap; + URLBase: string; Modals: BSModals; cssFile: string; availableProfiles: Array; diff --git a/views.go b/views.go index 30814ae..5164fd1 100644 --- a/views.go +++ b/views.go @@ -7,12 +7,18 @@ import ( "github.com/gin-gonic/gin" ) +func gcHTML(gc *gin.Context, code int, file string, templ gin.H) { + gc.Header("Cache-Control", "no-cache") + gc.HTML(code, file, templ) +} + func (app *appContext) AdminPage(gc *gin.Context) { bs5 := app.config.Section("ui").Key("bs5").MustBool(false) emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) - gc.HTML(http.StatusOK, "admin.html", gin.H{ + gcHTML(gc, http.StatusOK, "admin.html", gin.H{ + "urlBase": app.URLBase, "bs5": bs5, "cssFile": app.cssFile, "contactMessage": "", @@ -34,7 +40,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) { if strings.Contains(email, "Failed") { email = "" } - gc.HTML(http.StatusOK, "form-loader.html", gin.H{ + gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{ + "urlBase": app.URLBase, "cssFile": app.cssFile, "contactMessage": app.config.Section("ui").Key("contact_message").String(), "helpMessage": app.config.Section("ui").Key("help_message").String(), @@ -50,7 +57,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { "lang": app.storage.lang.Form["strings"], }) } else { - gc.HTML(404, "invalidCode.html", gin.H{ + gcHTML(gc, 404, "invalidCode.html", gin.H{ "bs5": app.config.Section("ui").Key("bs5").MustBool(false), "cssFile": app.cssFile, "contactMessage": app.config.Section("ui").Key("contact_message").String(), @@ -59,7 +66,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { } func (app *appContext) NoRouteHandler(gc *gin.Context) { - gc.HTML(404, "404.html", gin.H{ + gcHTML(gc, 404, "404.html", gin.H{ "bs5": app.config.Section("ui").Key("bs5").MustBool(false), "cssFile": app.cssFile, "contactMessage": app.config.Section("ui").Key("contact_message").String(),