1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 09:00:10 +00:00

add URL base option for subfolder proxies

also cleaned up the naming of some things.
This commit is contained in:
Harvey Tindall 2020-11-22 16:36:43 +00:00
parent e35d0579c8
commit 9dbf60e3df
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
15 changed files with 145 additions and 115 deletions

65
api.go
View File

@ -105,12 +105,12 @@ func timeDiff(a, b time.Time) (year, month, day, hour, min, sec int) {
} }
func (app *appContext) checkInvites() { func (app *appContext) checkInvites() {
current_time := time.Now() currentTime := time.Now()
app.storage.loadInvites() app.storage.loadInvites()
changed := false changed := false
for code, data := range app.storage.invites { for code, data := range app.storage.invites {
expiry := data.ValidTill expiry := data.ValidTill
if !current_time.After(expiry) { if !currentTime.After(expiry) {
continue continue
} }
app.debug.Printf("Housekeeping: Deleting old invite %s", code) 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 { func (app *appContext) checkInvite(code string, used bool, username string) bool {
current_time := time.Now() currentTime := time.Now()
app.storage.loadInvites() app.storage.loadInvites()
changed := false changed := false
inv, match := app.storage.invites[code] inv, match := app.storage.invites[code]
@ -152,7 +152,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
return false return false
} }
expiry := inv.ValidTill expiry := inv.ValidTill
if current_time.After(expiry) { if currentTime.After(expiry) {
app.debug.Printf("Housekeeping: Deleting old invite %s", code) app.debug.Printf("Housekeeping: Deleting old invite %s", code)
notify := inv.Notify notify := inv.Notify
if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { 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? // 0 means infinite i guess?
newInv.RemainingUses -= 1 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 { if !del {
app.storage.invites[code] = newInv app.storage.invites[code] = newInv
} }
@ -256,15 +256,17 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
} }
if len(app.storage.policy) != 0 { if len(app.storage.policy) != 0 {
status, err = app.jf.SetPolicy(id, app.storage.policy) 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.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 { if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 {
status, err = app.jf.SetConfiguration(id, app.storage.configuration) status, err = app.jf.SetConfiguration(id, app.storage.configuration)
if (status == 200 || status == 204) && err == nil { if (status == 200 || status == 204) && err == nil {
status, err = app.jf.SetDisplayPreferences(id, app.storage.displayprefs) 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) 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 { if len(profile.Policy) != 0 {
app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile)
status, err = app.jf.SetPolicy(id, profile.Policy) 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.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 { 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) status, err = app.jf.SetConfiguration(id, profile.Configuration)
if (status == 200 || status == 204) && err == nil { if (status == 200 || status == 204) && err == nil {
status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) 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.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.debug.Println("Generating new invite")
app.storage.loadInvites() app.storage.loadInvites()
gc.BindJSON(&req) gc.BindJSON(&req)
current_time := time.Now() currentTime := time.Now()
valid_till := current_time.AddDate(0, 0, req.Days) validTill := currentTime.AddDate(0, 0, req.Days)
valid_till = valid_till.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes)) validTill = validTill.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes))
// make sure code doesn't begin with number // make sure code doesn't begin with number
invite_code := shortuuid.New() inviteCode := shortuuid.New()
_, err := strconv.Atoi(string(invite_code[0])) _, err := strconv.Atoi(string(inviteCode[0]))
for err == nil { for err == nil {
invite_code = shortuuid.New() inviteCode = shortuuid.New()
_, err = strconv.Atoi(string(invite_code[0])) _, err = strconv.Atoi(string(inviteCode[0]))
} }
var invite Invite var invite Invite
invite.Created = current_time invite.Created = currentTime
if req.MultipleUses { if req.MultipleUses {
if req.NoLimit { if req.NoLimit {
invite.NoLimit = true invite.NoLimit = true
@ -503,21 +508,21 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
} else { } else {
invite.RemainingUses = 1 invite.RemainingUses = 1
} }
invite.ValidTill = valid_till invite.ValidTill = validTill
if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { 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 invite.Email = req.Email
msg, err := app.email.constructInvite(invite_code, invite, app) msg, err := app.email.constructInvite(inviteCode, invite, app)
if err != nil { if err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
app.err.Printf("%s: Failed to construct invite email", invite_code) app.err.Printf("%s: Failed to construct invite email", inviteCode)
app.debug.Printf("%s: Error: %s", invite_code, err) app.debug.Printf("%s: Error: %s", inviteCode, err)
} else if err := app.email.send(req.Email, msg); err != nil { } else if err := app.email.send(req.Email, msg); err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
app.err.Printf("%s: %s", invite_code, invite.Email) app.err.Printf("%s: %s", inviteCode, invite.Email)
app.debug.Printf("%s: Error: %s", invite_code, err) app.debug.Printf("%s: Error: %s", inviteCode, err)
} else { } 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 != "" { if req.Profile != "" {
@ -527,7 +532,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
invite.Profile = "Default" invite.Profile = "Default"
} }
} }
app.storage.invites[invite_code] = invite app.storage.invites[inviteCode] = invite
app.storage.storeInvites() app.storage.storeInvites()
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -972,7 +977,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
// @Produce json // @Produce json
// @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings" // @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings"
// @Success 200 {object} errorListDTO // @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] // @Router /users/settings [post]
// @Security Bearer // @Security Bearer
// @tags Profiles & Settings // @tags Profiles & Settings
@ -1063,7 +1068,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
func (app *appContext) GetConfig(gc *gin.Context) { func (app *appContext) GetConfig(gc *gin.Context) {
app.info.Println("Config requested") app.info.Println("Config requested")
resp := map[string]interface{}{} 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.langFiles, _ = ioutil.ReadDir(langPath)
app.lang.langOptions = make([]string, len(app.lang.langFiles)) app.lang.langOptions = make([]string, len(app.lang.langFiles))
chosenLang := app.config.Section("ui").Key("language").MustString("en-us") + ".json" 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") app.info.Println("Config modification requested")
var req configDTO var req configDTO
gc.BindJSON(&req) gc.BindJSON(&req)
tempConfig, _ := ini.Load(app.config_path) tempConfig, _ := ini.Load(app.configPath)
for section, settings := range req { for section, settings := range req {
if section != "restart-program" { if section != "restart-program" {
_, err := tempConfig.GetSection(section) _, 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") app.debug.Println("Config saved")
gc.JSON(200, map[string]bool{"success": true}) gc.JSON(200, map[string]bool{"success": true})
if req["restart-program"] != nil && req["restart-program"].(bool) { if req["restart-program"] != nil && req["restart-program"].(bool) {

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
) )
@ -36,7 +37,7 @@ func (app *appContext) loadDefaults() (err error) {
func (app *appContext) loadConfig() error { func (app *appContext) loadConfig() error {
var err error var err error
app.config, err = ini.Load(app.config_path) app.config, err = ini.Load(app.configPath)
if err != nil { if err != nil {
return err return err
} }
@ -48,32 +49,32 @@ func (app *appContext) loadConfig() error {
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
// } // }
if key.Name() != "html_templates" { 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"} { for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template"} {
// if app.config.Section("files").Key(key).MustString("") == "" { // if app.config.Section("files").Key(key).MustString("") == "" {
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) // 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("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_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.local_path, "email.txt"))) 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_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.local_path, "invite-email.txt"))) 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_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.local_path, "expired.txt"))) 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_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.local_path, "created.txt"))) 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_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.local_path, "deleted.txt"))) 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("version").SetValue(VERSION)
app.config.Section("jellyfin").Key("device").SetValue("jfa-go") app.config.Section("jellyfin").Key("device").SetValue("jfa-go")

View File

@ -179,6 +179,14 @@
"type": "bool", "type": "bool",
"value": false, "value": false,
"description": "Use the Bootstrap 5 Alpha. Looks better and removes the need for jQuery, so the page should load faster." "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": { "password_validation": {

View File

@ -4,7 +4,7 @@ import "time"
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS // https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
type Repeater struct { type repeater struct {
Stopped bool Stopped bool
ShutdownChannel chan string ShutdownChannel chan string
Interval time.Duration Interval time.Duration
@ -12,8 +12,8 @@ type Repeater struct {
app *appContext app *appContext
} }
func NewRepeater(interval time.Duration, app *appContext) *Repeater { func newRepeater(interval time.Duration, app *appContext) *repeater {
return &Repeater{ return &repeater{
Stopped: false, Stopped: false,
ShutdownChannel: make(chan string), ShutdownChannel: make(chan string),
Interval: interval, 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") rt.app.info.Println("Invite daemon started")
for { for {
select { select {
@ -42,7 +42,7 @@ func (rt *Repeater) Run() {
} }
} }
func (rt *Repeater) Shutdown() { func (rt *repeater) shutdown() {
rt.Stopped = true rt.Stopped = true
rt.ShutdownChannel <- "Down" rt.ShutdownChannel <- "Down"
<-rt.ShutdownChannel <-rt.ShutdownChannel

View File

@ -5,11 +5,11 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="{{ .urlBase }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="{{ .urlBase }}/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="{{ .urlBase }}/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest"> <link rel="manifest" href="{{ .urlBase }}/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> <link rel="mask-icon" href="{{ .urlBase }}/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#603cba"> <meta name="msapplication-TileColor" content="#603cba">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
@ -30,6 +30,7 @@
} }
return ""; return "";
} }
window.URLBase = "{{ .urlBase }}";
{{ if .bs5 }} {{ if .bs5 }}
window.bsVersion = 5; window.bsVersion = 5;
{{ else }} {{ else }}

View File

@ -4,6 +4,7 @@
window.usernameEnabled = {{ .settings.username }}; window.usernameEnabled = {{ .settings.username }};
window.validationStrings = JSON.parse({{ .lang.validationStrings }}); window.validationStrings = JSON.parse({{ .lang.validationStrings }});
window.invalidPassword = "{{ .lang.reEnterPasswordInvalid }}"; window.invalidPassword = "{{ .lang.reEnterPasswordInvalid }}";
window.URLBase = "{{ .urlBase }}";
</script> </script>
<script src="form.js" type="module"></script> <script src="form.js" type="module"></script>
{{ end }} {{ end }}

View File

@ -4,11 +4,11 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="{{ .urlBase }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="{{ .urlBase }}/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="{{ .urlBase }}/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest"> <link rel="manifest" href="{{ .urlBase }}/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> <link rel="mask-icon" href="{{ .urlBase }}/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#603cba"> <meta name="msapplication-TileColor" content="#603cba">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">

View File

@ -252,7 +252,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
return email, nil 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{ email := &Email{
subject: app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin"), subject: app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin"),
} }

90
main.go
View File

@ -34,7 +34,7 @@ import (
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
) )
// Username is JWT! // User is used for auth purposes.
type User struct { type User struct {
UserID string `json:"id"` UserID string `json:"id"`
Username string `json:"username"` Username string `json:"username"`
@ -44,11 +44,11 @@ type User struct {
type appContext struct { type appContext struct {
// defaults *Config // defaults *Config
config *ini.File config *ini.File
config_path string configPath string
configBase_path string configBasePath string
configBase map[string]interface{} configBase map[string]interface{}
data_path string dataPath string
local_path string localPath string
cssFile string cssFile string
bsVersion int bsVersion int
jellyfinLogin bool jellyfinLogin bool
@ -68,8 +68,10 @@ type appContext struct {
version string version string
quit chan os.Signal quit chan os.Signal
lang Languages 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 { type Languages struct {
langFiles []os.FileInfo // Language filenames langFiles []os.FileInfo // Language filenames
langOptions []string // Language names langOptions []string // Language names
@ -78,10 +80,10 @@ type Languages struct {
func (app *appContext) loadHTML(router *gin.Engine) { func (app *appContext) loadHTML(router *gin.Engine) {
customPath := app.config.Section("files").Key("html_templates").MustString("") 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) htmlFiles, err := ioutil.ReadDir(templatePath)
if err != nil { 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 return
} }
loadFiles := make([]string, len(htmlFiles)) loadFiles := make([]string, len(htmlFiles))
@ -97,7 +99,7 @@ func (app *appContext) loadHTML(router *gin.Engine) {
router.LoadHTMLFiles(loadFiles...) router.LoadHTMLFiles(loadFiles...)
} }
func GenerateSecret(length int) (string, error) { func generateSecret(length int) (string, error) {
bytes := make([]byte, length) bytes := make([]byte, length)
_, err := rand.Read(bytes) _, err := rand.Read(bytes)
if err != nil { if err != nil {
@ -173,7 +175,7 @@ func test(app *appContext) {
fmt.Scanln(&username) fmt.Scanln(&username)
user, status, err := app.jf.UserByName(username, false) user, status, err := app.jf.UserByName(username, false)
fmt.Printf("UserByName (%s): code %d err %s", username, status, err) fmt.Printf("UserByName (%s): code %d err %s", username, status, err)
out, err := json.MarshalIndent(user, "", " ") out, _ := json.MarshalIndent(user, "", " ")
fmt.Print(string(out)) fmt.Print(string(out))
} }
@ -187,17 +189,17 @@ func start(asDaemon, firstCall bool) {
local_path is the internal 'data' directory. local_path is the internal 'data' directory.
*/ */
userConfigDir, _ := os.UserConfigDir() userConfigDir, _ := os.UserConfigDir()
app.data_path = filepath.Join(userConfigDir, "jfa-go") app.dataPath = filepath.Join(userConfigDir, "jfa-go")
app.config_path = filepath.Join(app.data_path, "config.ini") app.configPath = filepath.Join(app.dataPath, "config.ini")
executable, _ := os.Executable() 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.info = log.New(os.Stdout, "[INFO] ", log.Ltime)
app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile) app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile)
if firstCall { if firstCall {
DATA = flag.String("data", app.data_path, "alternate path to data directory.") DATA = flag.String("data", app.dataPath, "alternate path to data directory.")
CONFIG = flag.String("config", app.config_path, "alternate path to config file.") CONFIG = flag.String("config", app.configPath, "alternate path to config file.")
HOST = flag.String("host", "", "alternate address to host web ui on.") HOST = flag.String("host", "", "alternate address to host web ui on.")
PORT = flag.Int("port", 0, "alternate port 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.") DEBUG = flag.Bool("debug", false, "Enables debug logging and exposes pprof.")
@ -219,35 +221,35 @@ func start(asDaemon, firstCall bool) {
*DEBUG = true *DEBUG = true
} }
// attempt to apply command line flags correctly // attempt to apply command line flags correctly
if app.config_path == *CONFIG && app.data_path != *DATA { if app.configPath == *CONFIG && app.dataPath != *DATA {
app.data_path = *DATA app.dataPath = *DATA
app.config_path = filepath.Join(app.data_path, "config.ini") app.configPath = filepath.Join(app.dataPath, "config.ini")
} else if app.config_path != *CONFIG && app.data_path == *DATA { } else if app.configPath != *CONFIG && app.dataPath == *DATA {
app.config_path = *CONFIG app.configPath = *CONFIG
} else { } else {
app.config_path = *CONFIG app.configPath = *CONFIG
app.data_path = *DATA app.dataPath = *DATA
} }
// env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason. // 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 != "" { if v := os.Getenv("JFA_CONFIGPATH"); v != "" {
app.config_path = v app.configPath = v
} }
if v := os.Getenv("JFA_DATAPATH"); 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_CONFIGPATH", app.configPath)
os.Setenv("JFA_DATAPATH", app.data_path) os.Setenv("JFA_DATAPATH", app.dataPath)
var firstRun bool var firstRun bool
if _, err := os.Stat(app.data_path); os.IsNotExist(err) { if _, err := os.Stat(app.dataPath); os.IsNotExist(err) {
os.Mkdir(app.data_path, 0700) 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 firstRun = true
dConfigPath := filepath.Join(app.local_path, "config-default.ini") dConfigPath := filepath.Join(app.localPath, "config-default.ini")
var dConfig *os.File var dConfig *os.File
dConfig, err = os.Open(dConfigPath) dConfig, err = os.Open(dConfigPath)
if err != nil { if err != nil {
@ -255,28 +257,28 @@ func start(asDaemon, firstCall bool) {
} }
defer dConfig.Close() defer dConfig.Close()
var nConfig *os.File var nConfig *os.File
nConfig, err := os.Create(app.config_path) nConfig, err := os.Create(app.configPath)
if err != nil { 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) app.err.Fatalf("Error: %s", err)
} }
defer nConfig.Close() defer nConfig.Close()
_, err = io.Copy(nConfig, dConfig) _, err = io.Copy(nConfig, dConfig)
if err != nil { 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 debugMode bool
var address string var address string
if app.loadConfig() != nil { 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") 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) { 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.storage.loadLang()
app.version = app.config.Section("jellyfin").Key("version").String() 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) 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) { if app.config.Section("ui").Key("bs5").MustBool(false) {
app.cssFile = "bs5-jf.css" 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") app.configBasePath = filepath.Join(app.localPath, "config-base.json")
configBase, _ := ioutil.ReadFile(app.configBase_path) configBase, _ := ioutil.ReadFile(app.configBasePath)
json.Unmarshal(configBase, &app.configBase) json.Unmarshal(configBase, &app.configBase)
themes := map[string]string{ themes := map[string]string{
@ -424,7 +426,7 @@ func start(asDaemon, firstCall bool) {
app.cssFile = val app.cssFile = val
} }
app.debug.Printf("Using css file \"%s\"", app.cssFile) app.debug.Printf("Using css file \"%s\"", app.cssFile)
secret, err := GenerateSecret(16) secret, err := generateSecret(16)
if err != nil { if err != nil {
app.err.Fatal(err) app.err.Fatal(err)
} }
@ -481,8 +483,8 @@ func start(asDaemon, firstCall bool) {
os.Exit(0) os.Exit(0)
} }
inviteDaemon := NewRepeater(time.Duration(60*time.Second), app) inviteDaemon := newRepeater(time.Duration(60*time.Second), app)
go inviteDaemon.Run() go inviteDaemon.run()
if app.config.Section("password_resets").Key("enabled").MustBool(false) { if app.config.Section("password_resets").Key("enabled").MustBool(false) {
go app.StartPWR() go app.StartPWR()
@ -502,7 +504,7 @@ func start(asDaemon, firstCall bool) {
setGinLogger(router, debugMode) setGinLogger(router, debugMode)
router.Use(gin.Recovery()) 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) app.loadHTML(router)
router.NoRoute(app.NoRouteHandler) router.NoRoute(app.NoRouteHandler)
if debugMode { if debugMode {
@ -514,7 +516,7 @@ func start(asDaemon, firstCall bool) {
router.GET("/token/login", app.getTokenLogin) router.GET("/token/login", app.getTokenLogin)
router.GET("/token/refresh", app.getTokenRefresh) router.GET("/token/refresh", app.getTokenRefresh)
router.POST("/newUser", app.NewUser) 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) router.GET("/invite/:invCode", app.InviteProxy)
if *SWAGGER { if *SWAGGER {
app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))

View File

@ -34,7 +34,8 @@ func (app *appContext) StartPWR() {
<-done <-done
} }
type Pwr struct { // PasswordReset represents a passwordreset-xyz.json file generated by Jellyfin.
type PasswordReset struct {
Pin string `json:"Pin"` Pin string `json:"Pin"`
Username string `json:"UserName"` Username string `json:"UserName"`
Expiry time.Time `json:"ExpirationDate"` Expiry time.Time `json:"ExpirationDate"`
@ -48,7 +49,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
return return
} }
if event.Op&fsnotify.Write == fsnotify.Write && strings.Contains(event.Name, "passwordreset") { if event.Op&fsnotify.Write == fsnotify.Write && strings.Contains(event.Name, "passwordreset") {
var pwr Pwr var pwr PasswordReset
data, err := ioutil.ReadFile(event.Name) data, err := ioutil.ReadFile(event.Name)
if err != nil { if err != nil {
return return

View File

@ -4,6 +4,7 @@ import (
"unicode" "unicode"
) )
// Validator allows for validation of passwords.
type Validator struct { type Validator struct {
minLength, upper, lower, number, special int minLength, upper, lower, number, special int
criteria ValidatorConf criteria ValidatorConf

View File

@ -121,10 +121,12 @@ window.toClipboard = (str: string): void => {
function login(username: string, password: string, modal: boolean, button?: HTMLButtonElement, run?: (arg0: number) => void): void { function login(username: string, password: string, modal: boolean, button?: HTMLButtonElement, run?: (arg0: number) => void): void {
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
req.responseType = 'json'; req.responseType = 'json';
let url = "/token/login"; let url = window.URLBase;
const refresh = (username == "" && password == ""); const refresh = (username == "" && password == "");
if (refresh) { if (refresh) {
url = "/token/refresh"; url += "/token/refresh";
} else {
url += "/token/login";
} }
req.open("GET", url, true); req.open("GET", url, true);
if (!refresh) { if (!refresh) {

View File

@ -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 => { export const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
req.open("GET", url, true); req.open("GET", window.URLBase + url, true);
req.responseType = 'json'; req.responseType = 'json';
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); 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 => { export const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
req.open("POST", url, true); req.open("POST", window.URLBase + url, true);
if (response) { if (response) {
req.responseType = 'json'; 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 { export function _delete(url: string, data: Object, onreadystatechange: () => void): void {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
req.open("DELETE", url, true); req.open("DELETE", window.URLBase + url, true);
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange; req.onreadystatechange = onreadystatechange;

View File

@ -14,6 +14,7 @@ declare interface Window {
bsVersion: number; bsVersion: number;
bs5: boolean; bs5: boolean;
BS: Bootstrap; BS: Bootstrap;
URLBase: string;
Modals: BSModals; Modals: BSModals;
cssFile: string; cssFile: string;
availableProfiles: Array<any>; availableProfiles: Array<any>;

View File

@ -7,12 +7,18 @@ import (
"github.com/gin-gonic/gin" "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) { func (app *appContext) AdminPage(gc *gin.Context) {
bs5 := app.config.Section("ui").Key("bs5").MustBool(false) bs5 := app.config.Section("ui").Key("bs5").MustBool(false)
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) 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, "bs5": bs5,
"cssFile": app.cssFile, "cssFile": app.cssFile,
"contactMessage": "", "contactMessage": "",
@ -34,7 +40,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
if strings.Contains(email, "Failed") { if strings.Contains(email, "Failed") {
email = "" 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, "cssFile": app.cssFile,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
"helpMessage": app.config.Section("ui").Key("help_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"], "lang": app.storage.lang.Form["strings"],
}) })
} else { } else {
gc.HTML(404, "invalidCode.html", gin.H{ gcHTML(gc, 404, "invalidCode.html", gin.H{
"bs5": app.config.Section("ui").Key("bs5").MustBool(false), "bs5": app.config.Section("ui").Key("bs5").MustBool(false),
"cssFile": app.cssFile, "cssFile": app.cssFile,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "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) { 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), "bs5": app.config.Section("ui").Key("bs5").MustBool(false),
"cssFile": app.cssFile, "cssFile": app.cssFile,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),