use app identifier instead of ctx

changing this because ctx is commonly used with the context package.
pull/20/head
Harvey Tindall 4 years ago
parent fffb3471d6
commit fd766e7b1a
No known key found for this signature in database
GPG Key ID: BBC65952848FB1A2

349
api.go

@ -2,33 +2,34 @@ package main
import ( import (
"fmt" "fmt"
"github.com/gin-gonic/gin"
"github.com/knz/strtime"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/knz/strtime"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
) )
func (ctx *appContext) loadStrftime() { func (app *appContext) loadStrftime() {
ctx.datePattern = ctx.config.Section("email").Key("date_format").String() app.datePattern = app.config.Section("email").Key("date_format").String()
ctx.timePattern = `%H:%M` app.timePattern = `%H:%M`
if val, _ := ctx.config.Section("email").Key("use_24h").Bool(); !val { if val, _ := app.config.Section("email").Key("use_24h").Bool(); !val {
ctx.timePattern = `%I:%M %p` app.timePattern = `%I:%M %p`
} }
return return
} }
func (ctx *appContext) prettyTime(dt time.Time) (date, time string) { func (app *appContext) prettyTime(dt time.Time) (date, time string) {
date, _ = strtime.Strftime(dt, ctx.datePattern) date, _ = strtime.Strftime(dt, app.datePattern)
time, _ = strtime.Strftime(dt, ctx.timePattern) time, _ = strtime.Strftime(dt, app.timePattern)
return return
} }
func (ctx *appContext) formatDatetime(dt time.Time) string { func (app *appContext) formatDatetime(dt time.Time) string {
d, t := ctx.prettyTime(dt) d, t := app.prettyTime(dt)
return d + " " + t return d + " " + t
} }
@ -79,60 +80,60 @@ func timeDiff(a, b time.Time) (year, month, day, hour, min, sec int) {
return return
} }
func (ctx *appContext) checkInvites() { func (app *appContext) checkInvites() {
current_time := time.Now() current_time := time.Now()
ctx.storage.loadInvites() app.storage.loadInvites()
changed := false changed := false
for code, data := range ctx.storage.invites { for code, data := range app.storage.invites {
expiry := data.ValidTill expiry := data.ValidTill
if current_time.After(expiry) { if current_time.After(expiry) {
ctx.debug.Printf("Housekeeping: Deleting old invite %s", code) app.debug.Printf("Housekeeping: Deleting old invite %s", code)
notify := data.Notify notify := data.Notify
if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
ctx.debug.Printf("%s: Expiry notification", code) app.debug.Printf("%s: Expiry notification", code)
for address, settings := range notify { for address, settings := range notify {
if settings["notify-expiry"] { if settings["notify-expiry"] {
go func() { go func() {
if ctx.email.constructExpiry(code, data, ctx) != nil { if app.email.constructExpiry(code, data, app) != nil {
ctx.err.Printf("%s: Failed to construct expiry notification", code) app.err.Printf("%s: Failed to construct expiry notification", code)
} else if ctx.email.send(address, ctx) != nil { } else if app.email.send(address, app) != nil {
ctx.err.Printf("%s: Failed to send expiry notification", code) app.err.Printf("%s: Failed to send expiry notification", code)
} else { } else {
ctx.info.Printf("Sent expiry notification to %s", address) app.info.Printf("Sent expiry notification to %s", address)
} }
}() }()
} }
} }
} }
changed = true changed = true
delete(ctx.storage.invites, code) delete(app.storage.invites, code)
} }
} }
if changed { if changed {
ctx.storage.storeInvites() app.storage.storeInvites()
} }
} }
func (ctx *appContext) checkInvite(code string, used bool, username string) bool { func (app *appContext) checkInvite(code string, used bool, username string) bool {
current_time := time.Now() current_time := time.Now()
ctx.storage.loadInvites() app.storage.loadInvites()
changed := false changed := false
if inv, match := ctx.storage.invites[code]; match { if inv, match := app.storage.invites[code]; match {
expiry := inv.ValidTill expiry := inv.ValidTill
if current_time.After(expiry) { if current_time.After(expiry) {
ctx.debug.Printf("Housekeeping: Deleting old invite %s", code) app.debug.Printf("Housekeeping: Deleting old invite %s", code)
notify := inv.Notify notify := inv.Notify
if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
ctx.debug.Printf("%s: Expiry notification", code) app.debug.Printf("%s: Expiry notification", code)
for address, settings := range notify { for address, settings := range notify {
if settings["notify-expiry"] { if settings["notify-expiry"] {
go func() { go func() {
if ctx.email.constructExpiry(code, inv, ctx) != nil { if app.email.constructExpiry(code, inv, app) != nil {
ctx.err.Printf("%s: Failed to construct expiry notification", code) app.err.Printf("%s: Failed to construct expiry notification", code)
} else if ctx.email.send(address, ctx) != nil { } else if app.email.send(address, app) != nil {
ctx.err.Printf("%s: Failed to send expiry notification", code) app.err.Printf("%s: Failed to send expiry notification", code)
} else { } else {
ctx.info.Printf("Sent expiry notification to %s", address) app.info.Printf("Sent expiry notification to %s", address)
} }
}() }()
} }
@ -140,25 +141,25 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool
} }
changed = true changed = true
match = false match = false
delete(ctx.storage.invites, code) delete(app.storage.invites, code)
} else if used { } else if used {
changed = true changed = true
del := false del := false
newInv := inv newInv := inv
if newInv.RemainingUses == 1 { if newInv.RemainingUses == 1 {
del = true del = true
delete(ctx.storage.invites, code) delete(app.storage.invites, code)
} else if newInv.RemainingUses != 0 { } else if newInv.RemainingUses != 0 {
// 0 means infinite i guess? // 0 means infinite i guess?
newInv.RemainingUses -= 1 newInv.RemainingUses -= 1
} }
newInv.UsedBy = append(newInv.UsedBy, []string{username, ctx.formatDatetime(current_time)}) newInv.UsedBy = append(newInv.UsedBy, []string{username, app.formatDatetime(current_time)})
if !del { if !del {
ctx.storage.invites[code] = newInv app.storage.invites[code] = newInv
} }
} }
if changed { if changed {
ctx.storage.storeInvites() app.storage.storeInvites()
} }
return match return match
} }
@ -174,17 +175,17 @@ type newUserReq struct {
Code string `json:"code"` Code string `json:"code"`
} }
func (ctx *appContext) NewUser(gc *gin.Context) { func (app *appContext) NewUser(gc *gin.Context) {
var req newUserReq var req newUserReq
gc.BindJSON(&req) gc.BindJSON(&req)
ctx.debug.Printf("%s: New user attempt", req.Code) app.debug.Printf("%s: New user attempt", req.Code)
if !ctx.checkInvite(req.Code, false, "") { if !app.checkInvite(req.Code, false, "") {
ctx.info.Printf("%s New user failed: invalid code", req.Code) app.info.Printf("%s New user failed: invalid code", req.Code)
gc.JSON(401, map[string]bool{"success": false}) gc.JSON(401, map[string]bool{"success": false})
gc.Abort() gc.Abort()
return return
} }
validation := ctx.validator.validate(req.Password) validation := app.validator.validate(req.Password)
valid := true valid := true
for _, val := range validation { for _, val := range validation {
if !val { if !val {
@ -193,38 +194,38 @@ func (ctx *appContext) NewUser(gc *gin.Context) {
} }
if !valid { if !valid {
// 200 bcs idk what i did in js // 200 bcs idk what i did in js
ctx.info.Printf("%s New user failed: Invalid password", req.Code) app.info.Printf("%s New user failed: Invalid password", req.Code)
gc.JSON(200, validation) gc.JSON(200, validation)
gc.Abort() gc.Abort()
return return
} }
existingUser, _, _ := ctx.jf.userByName(req.Username, false) existingUser, _, _ := app.jf.userByName(req.Username, false)
if existingUser != nil { if existingUser != nil {
msg := fmt.Sprintf("User already exists named %s", req.Username) msg := fmt.Sprintf("User already exists named %s", req.Username)
ctx.info.Printf("%s New user failed: %s", req.Code, msg) app.info.Printf("%s New user failed: %s", req.Code, msg)
respond(401, msg, gc) respond(401, msg, gc)
return return
} }
user, status, err := ctx.jf.newUser(req.Username, req.Password) user, status, err := app.jf.newUser(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
ctx.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status) app.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status)
respond(401, "Unknown error", gc) respond(401, "Unknown error", gc)
return return
} }
ctx.checkInvite(req.Code, true, req.Username) app.checkInvite(req.Code, true, req.Username)
invite := ctx.storage.invites[req.Code] invite := app.storage.invites[req.Code]
if ctx.config.Section("notifications").Key("enabled").MustBool(false) { if app.config.Section("notifications").Key("enabled").MustBool(false) {
for address, settings := range invite.Notify { for address, settings := range invite.Notify {
if settings["notify-creation"] { if settings["notify-creation"] {
go func() { go func() {
if ctx.email.constructCreated(req.Code, req.Username, req.Email, invite, ctx) != nil { if app.email.constructCreated(req.Code, req.Username, req.Email, invite, app) != nil {
ctx.err.Printf("%s: Failed to construct user creation notification", req.Code) app.err.Printf("%s: Failed to construct user creation notification", req.Code)
ctx.debug.Printf("%s: Error: %s", req.Code, err) app.debug.Printf("%s: Error: %s", req.Code, err)
} else if ctx.email.send(address, ctx) != nil { } else if app.email.send(address, app) != nil {
ctx.err.Printf("%s: Failed to send user creation notification", req.Code) app.err.Printf("%s: Failed to send user creation notification", req.Code)
ctx.debug.Printf("%s: Error: %s", req.Code, err) app.debug.Printf("%s: Error: %s", req.Code, err)
} else { } else {
ctx.info.Printf("%s: Sent user creation notification to %s", req.Code, address) app.info.Printf("%s: Sent user creation notification to %s", req.Code, address)
} }
}() }()
} }
@ -234,23 +235,23 @@ func (ctx *appContext) NewUser(gc *gin.Context) {
if user["Id"] != nil { if user["Id"] != nil {
id = user["Id"].(string) id = user["Id"].(string)
} }
if len(ctx.storage.policy) != 0 { if len(app.storage.policy) != 0 {
status, err = ctx.jf.setPolicy(id, ctx.storage.policy) status, err = app.jf.setPolicy(id, app.storage.policy)
if !(status == 200 || status == 204) { if !(status == 200 || status == 204) {
ctx.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)
} }
} }
if len(ctx.storage.configuration) != 0 && len(ctx.storage.displayprefs) != 0 { if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 {
status, err = ctx.jf.setConfiguration(id, ctx.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 = ctx.jf.setDisplayPreferences(id, ctx.storage.displayprefs) status, err = app.jf.setDisplayPreferences(id, app.storage.displayprefs)
} else { } else {
ctx.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)
} }
} }
if ctx.config.Section("password_resets").Key("enabled").MustBool(false) { if app.config.Section("password_resets").Key("enabled").MustBool(false) {
ctx.storage.emails[id] = req.Email app.storage.emails[id] = req.Email
ctx.storage.storeEmails() app.storage.storeEmails()
} }
gc.JSON(200, validation) gc.JSON(200, validation)
} }
@ -265,10 +266,10 @@ type generateInviteReq struct {
RemainingUses int `json:"remaining-uses"` RemainingUses int `json:"remaining-uses"`
} }
func (ctx *appContext) GenerateInvite(gc *gin.Context) { func (app *appContext) GenerateInvite(gc *gin.Context) {
var req generateInviteReq var req generateInviteReq
ctx.debug.Println("Generating new invite") app.debug.Println("Generating new invite")
ctx.storage.loadInvites() app.storage.loadInvites()
gc.BindJSON(&req) gc.BindJSON(&req)
current_time := time.Now() current_time := time.Now()
valid_till := current_time.AddDate(0, 0, req.Days) valid_till := current_time.AddDate(0, 0, req.Days)
@ -286,40 +287,40 @@ func (ctx *appContext) GenerateInvite(gc *gin.Context) {
invite.RemainingUses = 1 invite.RemainingUses = 1
} }
invite.ValidTill = valid_till invite.ValidTill = valid_till
if req.Email != "" && ctx.config.Section("invite_emails").Key("enabled").MustBool(false) { if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
ctx.debug.Printf("%s: Sending invite email", invite_code) app.debug.Printf("%s: Sending invite email", invite_code)
invite.Email = req.Email invite.Email = req.Email
if err := ctx.email.constructInvite(invite_code, invite, ctx); err != nil { if err := app.email.constructInvite(invite_code, invite, app); err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
ctx.err.Printf("%s: Failed to construct invite email", invite_code) app.err.Printf("%s: Failed to construct invite email", invite_code)
ctx.debug.Printf("%s: Error: %s", invite_code, err) app.debug.Printf("%s: Error: %s", invite_code, err)
} else if err := ctx.email.send(req.Email, ctx); err != nil { } else if err := app.email.send(req.Email, app); err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
ctx.err.Printf("%s: %s", invite_code, invite.Email) app.err.Printf("%s: %s", invite_code, invite.Email)
ctx.debug.Printf("%s: Error: %s", invite_code, err) app.debug.Printf("%s: Error: %s", invite_code, err)
} else { } else {
ctx.info.Printf("%s: Sent invite email to %s", invite_code, req.Email) app.info.Printf("%s: Sent invite email to %s", invite_code, req.Email)
} }
} }
ctx.storage.invites[invite_code] = invite app.storage.invites[invite_code] = invite
ctx.storage.storeInvites() app.storage.storeInvites()
gc.JSON(200, map[string]bool{"success": true}) gc.JSON(200, map[string]bool{"success": true})
} }
func (ctx *appContext) GetInvites(gc *gin.Context) { func (app *appContext) GetInvites(gc *gin.Context) {
ctx.debug.Println("Invites requested") app.debug.Println("Invites requested")
current_time := time.Now() current_time := time.Now()
ctx.storage.loadInvites() app.storage.loadInvites()
ctx.checkInvites() app.checkInvites()
var invites []map[string]interface{} var invites []map[string]interface{}
for code, inv := range ctx.storage.invites { for code, inv := range app.storage.invites {
_, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time) _, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time)
invite := make(map[string]interface{}) invite := make(map[string]interface{})
invite["code"] = code invite["code"] = code
invite["days"] = days invite["days"] = days
invite["hours"] = hours invite["hours"] = hours
invite["minutes"] = minutes invite["minutes"] = minutes
invite["created"] = ctx.formatDatetime(inv.Created) invite["created"] = app.formatDatetime(inv.Created)
if len(inv.UsedBy) != 0 { if len(inv.UsedBy) != 0 {
invite["used-by"] = inv.UsedBy invite["used-by"] = inv.UsedBy
} }
@ -335,11 +336,11 @@ func (ctx *appContext) GetInvites(gc *gin.Context) {
} }
if len(inv.Notify) != 0 { if len(inv.Notify) != 0 {
var address string var address string
if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) { if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
ctx.storage.loadEmails() app.storage.loadEmails()
address = ctx.storage.emails[gc.GetString("jfId")].(string) address = app.storage.emails[gc.GetString("jfId")].(string)
} else { } else {
address = ctx.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
} }
if _, ok := inv.Notify[address]; ok { if _, ok := inv.Notify[address]; ok {
for _, notify_type := range []string{"notify-expiry", "notify-creation"} { for _, notify_type := range []string{"notify-expiry", "notify-creation"} {
@ -362,34 +363,34 @@ type notifySetting struct {
NotifyCreation bool `json:"notify-creation"` NotifyCreation bool `json:"notify-creation"`
} }
func (ctx *appContext) SetNotify(gc *gin.Context) { func (app *appContext) SetNotify(gc *gin.Context) {
var req map[string]notifySetting var req map[string]notifySetting
gc.BindJSON(&req) gc.BindJSON(&req)
changed := false changed := false
for code, settings := range req { for code, settings := range req {
ctx.debug.Printf("%s: Notification settings change requested", code) app.debug.Printf("%s: Notification settings change requested", code)
ctx.storage.loadInvites() app.storage.loadInvites()
ctx.storage.loadEmails() app.storage.loadEmails()
invite, ok := ctx.storage.invites[code] invite, ok := app.storage.invites[code]
if !ok { if !ok {
ctx.err.Printf("%s Notification setting change failed: Invalid code", code) app.err.Printf("%s Notification setting change failed: Invalid code", code)
gc.JSON(400, map[string]string{"error": "Invalid invite code"}) gc.JSON(400, map[string]string{"error": "Invalid invite code"})
gc.Abort() gc.Abort()
return return
} }
var address string var address string
if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) { if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
var ok bool var ok bool
address, ok = ctx.storage.emails[gc.GetString("jfId")].(string) address, ok = app.storage.emails[gc.GetString("jfId")].(string)
if !ok { if !ok {
ctx.err.Printf("%s: Couldn't find email address. Make sure it's set", code) app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
ctx.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId")) app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
gc.JSON(500, map[string]string{"error": "Missing user email"}) gc.JSON(500, map[string]string{"error": "Missing user email"})
gc.Abort() gc.Abort()
return return
} }
} else { } else {
address = ctx.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
} }
if invite.Notify == nil { if invite.Notify == nil {
invite.Notify = map[string]map[string]bool{} invite.Notify = map[string]map[string]bool{}
@ -401,20 +402,20 @@ func (ctx *appContext) SetNotify(gc *gin.Context) {
*/ */
if invite.Notify[address]["notify-expiry"] != settings.NotifyExpiry { if invite.Notify[address]["notify-expiry"] != settings.NotifyExpiry {
invite.Notify[address]["notify-expiry"] = settings.NotifyExpiry invite.Notify[address]["notify-expiry"] = settings.NotifyExpiry
ctx.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings.NotifyExpiry, address) app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings.NotifyExpiry, address)
changed = true changed = true
} }
if invite.Notify[address]["notify-creation"] != settings.NotifyCreation { if invite.Notify[address]["notify-creation"] != settings.NotifyCreation {
invite.Notify[address]["notify-creation"] = settings.NotifyCreation invite.Notify[address]["notify-creation"] = settings.NotifyCreation
ctx.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings.NotifyExpiry, address) app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings.NotifyExpiry, address)
changed = true changed = true
} }
if changed { if changed {
ctx.storage.invites[code] = invite app.storage.invites[code] = invite
} }
} }
if changed { if changed {
ctx.storage.storeInvites() app.storage.storeInvites()
} }
} }
@ -422,20 +423,20 @@ type deleteReq struct {
Code string `json:"code"` Code string `json:"code"`
} }
func (ctx *appContext) DeleteInvite(gc *gin.Context) { func (app *appContext) DeleteInvite(gc *gin.Context) {
var req deleteReq var req deleteReq
gc.BindJSON(&req) gc.BindJSON(&req)
ctx.debug.Printf("%s: Deletion requested", req.Code) app.debug.Printf("%s: Deletion requested", req.Code)
var ok bool var ok bool
_, ok = ctx.storage.invites[req.Code] _, ok = app.storage.invites[req.Code]
if ok { if ok {
delete(ctx.storage.invites, req.Code) delete(app.storage.invites, req.Code)
ctx.storage.storeInvites() app.storage.storeInvites()
ctx.info.Printf("%s: Invite deleted", req.Code) app.info.Printf("%s: Invite deleted", req.Code)
gc.JSON(200, map[string]bool{"success": true}) gc.JSON(200, map[string]bool{"success": true})
return return
} }
ctx.err.Printf("%s: Deletion failed: Invalid code", req.Code) app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
respond(401, "Code doesn't exist", gc) respond(401, "Code doesn't exist", gc)
} }
@ -448,21 +449,21 @@ type respUser struct {
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
} }
func (ctx *appContext) GetUsers(gc *gin.Context) { func (app *appContext) GetUsers(gc *gin.Context) {
ctx.debug.Println("Users requested") app.debug.Println("Users requested")
var resp userResp var resp userResp
resp.UserList = []respUser{} resp.UserList = []respUser{}
users, status, err := ctx.jf.getUsers(false) users, status, err := app.jf.getUsers(false)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status) app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
ctx.debug.Printf("Error: %s", err) app.debug.Printf("Error: %s", err)
respond(500, "Couldn't get users", gc) respond(500, "Couldn't get users", gc)
return return
} }
for _, jfUser := range users { for _, jfUser := range users {
var user respUser var user respUser
user.Name = jfUser["Name"].(string) user.Name = jfUser["Name"].(string)
if email, ok := ctx.storage.emails[jfUser["Id"].(string)]; ok { if email, ok := app.storage.emails[jfUser["Id"].(string)]; ok {
user.Email = email.(string) user.Email = email.(string)
} }
resp.UserList = append(resp.UserList, user) resp.UserList = append(resp.UserList, user)
@ -470,24 +471,24 @@ func (ctx *appContext) GetUsers(gc *gin.Context) {
gc.JSON(200, resp) gc.JSON(200, resp)
} }
func (ctx *appContext) ModifyEmails(gc *gin.Context) { func (app *appContext) ModifyEmails(gc *gin.Context) {
var req map[string]string var req map[string]string
gc.BindJSON(&req) gc.BindJSON(&req)
ctx.debug.Println("Email modification requested") app.debug.Println("Email modification requested")
users, status, err := ctx.jf.getUsers(false) users, status, err := app.jf.getUsers(false)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status) app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
ctx.debug.Printf("Error: %s", err) app.debug.Printf("Error: %s", err)
respond(500, "Couldn't get users", gc) respond(500, "Couldn't get users", gc)
return return
} }
for _, jfUser := range users { for _, jfUser := range users {
if address, ok := req[jfUser["Name"].(string)]; ok { if address, ok := req[jfUser["Name"].(string)]; ok {
ctx.storage.emails[jfUser["Id"].(string)] = address app.storage.emails[jfUser["Id"].(string)] = address
} }
} }
ctx.storage.storeEmails() app.storage.storeEmails()
ctx.info.Println("Email list modified") app.info.Println("Email list modified")
gc.JSON(200, map[string]bool{"success": true}) gc.JSON(200, map[string]bool{"success": true})
} }
@ -496,46 +497,46 @@ type defaultsReq struct {
Homescreen bool `json:"homescreen"` Homescreen bool `json:"homescreen"`
} }
func (ctx *appContext) SetDefaults(gc *gin.Context) { func (app *appContext) SetDefaults(gc *gin.Context) {
var req defaultsReq var req defaultsReq
gc.BindJSON(&req) gc.BindJSON(&req)
ctx.info.Printf("Getting user defaults from \"%s\"", req.Username) app.info.Printf("Getting user defaults from \"%s\"", req.Username)
user, status, err := ctx.jf.userByName(req.Username, false) user, status, err := app.jf.userByName(req.Username, false)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
ctx.err.Printf("Failed to get user from Jellyfin: Code %d", status) app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
ctx.debug.Printf("Error: %s", err) app.debug.Printf("Error: %s", err)
respond(500, "Couldn't get user", gc) respond(500, "Couldn't get user", gc)
return return
} }
userId := user["Id"].(string) userId := user["Id"].(string)
policy := user["Policy"].(map[string]interface{}) policy := user["Policy"].(map[string]interface{})
ctx.storage.policy = policy app.storage.policy = policy
ctx.storage.storePolicy() app.storage.storePolicy()
ctx.debug.Println("User policy template stored") app.debug.Println("User policy template stored")
if req.Homescreen { if req.Homescreen {
configuration := user["Configuration"].(map[string]interface{}) configuration := user["Configuration"].(map[string]interface{})
var displayprefs map[string]interface{} var displayprefs map[string]interface{}
displayprefs, status, err = ctx.jf.getDisplayPreferences(userId) displayprefs, status, err = app.jf.getDisplayPreferences(userId)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
ctx.err.Printf("Failed to get DisplayPrefs: Code %d", status) app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
ctx.debug.Printf("Error: %s", err) app.debug.Printf("Error: %s", err)
respond(500, "Couldn't get displayprefs", gc) respond(500, "Couldn't get displayprefs", gc)
return return
} }
ctx.storage.configuration = configuration app.storage.configuration = configuration
ctx.storage.displayprefs = displayprefs app.storage.displayprefs = displayprefs
ctx.storage.storeConfiguration() app.storage.storeConfiguration()
ctx.debug.Println("Configuration template stored") app.debug.Println("Configuration template stored")
ctx.storage.storeDisplayprefs() app.storage.storeDisplayprefs()
ctx.debug.Println("DisplayPrefs template stored") app.debug.Println("DisplayPrefs template stored")
} }
gc.JSON(200, map[string]bool{"success": true}) gc.JSON(200, map[string]bool{"success": true})
} }
func (ctx *appContext) GetConfig(gc *gin.Context) { func (app *appContext) GetConfig(gc *gin.Context) {
ctx.info.Println("Config requested") app.info.Println("Config requested")
resp := map[string]interface{}{} resp := map[string]interface{}{}
for section, settings := range ctx.configBase { for section, settings := range app.configBase {
if section == "order" { if section == "order" {
resp[section] = settings.([]interface{}) resp[section] = settings.([]interface{})
} else { } else {
@ -547,7 +548,7 @@ func (ctx *appContext) GetConfig(gc *gin.Context) {
resp[section].(map[string]interface{})[key] = values.(map[string]interface{}) resp[section].(map[string]interface{})[key] = values.(map[string]interface{})
if key != "meta" { if key != "meta" {
dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string) dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string)
configKey := ctx.config.Section(section).Key(key) configKey := app.config.Section(section).Key(key)
if dataType == "number" { if dataType == "number" {
if val, err := configKey.Int(); err == nil { if val, err := configKey.Int(); err == nil {
resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val
@ -565,11 +566,11 @@ func (ctx *appContext) GetConfig(gc *gin.Context) {
gc.JSON(200, resp) gc.JSON(200, resp)
} }
func (ctx *appContext) ModifyConfig(gc *gin.Context) { func (app *appContext) ModifyConfig(gc *gin.Context) {
ctx.info.Println("Config modification requested") app.info.Println("Config modification requested")
var req map[string]interface{} var req map[string]interface{}
gc.BindJSON(&req) gc.BindJSON(&req)
tempConfig, _ := ini.Load(ctx.config_path) tempConfig, _ := ini.Load(app.config_path)
for section, settings := range req { for section, settings := range req {
_, err := tempConfig.GetSection(section) _, err := tempConfig.GetSection(section)
if section != "restart-program" && err == nil { if section != "restart-program" && err == nil {
@ -578,33 +579,33 @@ func (ctx *appContext) ModifyConfig(gc *gin.Context) {
} }
} }
} }
tempConfig.SaveTo(ctx.config_path) tempConfig.SaveTo(app.config_path)
ctx.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"].(bool) { if req["restart-program"].(bool) {
ctx.info.Println("Restarting...") app.info.Println("Restarting...")
err := ctx.Restart() err := app.Restart()
if err != nil { if err != nil {
ctx.err.Printf("Couldn't restart, try restarting manually. (%s)", err) app.err.Printf("Couldn't restart, try restarting manually. (%s)", err)
} }
} }
ctx.loadConfig() app.loadConfig()
// Reinitialize password validator on config change, as opposed to every applicable request like in python. // Reinitialize password validator on config change, as opposed to every applicable request like in python.
if _, ok := req["password_validation"]; ok { if _, ok := req["password_validation"]; ok {
ctx.debug.Println("Reinitializing validator") app.debug.Println("Reinitializing validator")
validatorConf := ValidatorConf{ validatorConf := ValidatorConf{
"characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0), "characters": app.config.Section("password_validation").Key("min_length").MustInt(0),
"uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0), "uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0),
"lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0), "lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0),
"numbers": ctx.config.Section("password_validation").Key("number").MustInt(0), "numbers": app.config.Section("password_validation").Key("number").MustInt(0),
"special characters": ctx.config.Section("password_validation").Key("special").MustInt(0), "special characters": app.config.Section("password_validation").Key("special").MustInt(0),
} }
if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) { if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
for key := range validatorConf { for key := range validatorConf {
validatorConf[key] = 0 validatorConf[key] = 0
} }
} }
ctx.validator.init(validatorConf) app.validator.init(validatorConf)
} }
} }
@ -634,11 +635,11 @@ func (ctx *appContext) ModifyConfig(gc *gin.Context) {
// panic(fmt.Errorf("restarting")) // panic(fmt.Errorf("restarting"))
// } // }
func (ctx *appContext) Restart() error { func (app *appContext) Restart() error {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
signal.Notify(ctx.quit, os.Interrupt) signal.Notify(app.quit, os.Interrupt)
<-ctx.quit <-app.quit
} }
}() }()
args := os.Args args := os.Args

@ -3,22 +3,23 @@ package main
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
) )
func (ctx *appContext) webAuth() gin.HandlerFunc { func (app *appContext) webAuth() gin.HandlerFunc {
return ctx.authenticate return app.authenticate
} }
func (ctx *appContext) authenticate(gc *gin.Context) { func (app *appContext) authenticate(gc *gin.Context) {
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
if header[0] != "Basic" { if header[0] != "Basic" {
ctx.debug.Println("Invalid authentication header") app.debug.Println("Invalid authentication header")
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
@ -26,13 +27,13 @@ func (ctx *appContext) authenticate(gc *gin.Context) {
creds := strings.SplitN(string(auth), ":", 2) creds := strings.SplitN(string(auth), ":", 2)
token, err := jwt.Parse(creds[0], func(token *jwt.Token) (interface{}, error) { token, err := jwt.Parse(creds[0], func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
ctx.debug.Printf("Invalid JWT signing method %s", token.Header["alg"]) app.debug.Printf("Invalid JWT signing method %s", token.Header["alg"])
return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"])
} }
return []byte(os.Getenv("JFA_SECRET")), nil return []byte(os.Getenv("JFA_SECRET")), nil
}) })
if err != nil { if err != nil {
ctx.debug.Printf("Auth denied: %s", err) app.debug.Printf("Auth denied: %s", err)
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
@ -43,32 +44,32 @@ func (ctx *appContext) authenticate(gc *gin.Context) {
userId = claims["id"].(string) userId = claims["id"].(string)
jfId = claims["jfid"].(string) jfId = claims["jfid"].(string)
} else { } else {
ctx.debug.Printf("Invalid token") app.debug.Printf("Invalid token")
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
match := false match := false
for _, user := range ctx.users { for _, user := range app.users {
if user.UserID == userId { if user.UserID == userId {
match = true match = true
} }
} }
if !match { if !match {
ctx.debug.Printf("Couldn't find user ID %s", userId) app.debug.Printf("Couldn't find user ID %s", userId)
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
gc.Set("jfId", jfId) gc.Set("jfId", jfId)
gc.Set("userId", userId) gc.Set("userId", userId)
ctx.debug.Println("Authentication successful") app.debug.Println("Authentication successful")
gc.Next() gc.Next()
} }
func (ctx *appContext) GetToken(gc *gin.Context) { func (app *appContext) GetToken(gc *gin.Context) {
ctx.info.Println("Token requested (login attempt)") app.info.Println("Token requested (login attempt)")
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
if header[0] != "Basic" { if header[0] != "Basic" {
ctx.debug.Println("Invalid authentication header") app.debug.Println("Invalid authentication header")
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
@ -76,7 +77,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
creds := strings.SplitN(string(auth), ":", 2) creds := strings.SplitN(string(auth), ":", 2)
match := false match := false
var userId string var userId string
for _, user := range ctx.users { for _, user := range app.users {
if user.Username == creds[0] && user.Password == creds[1] { if user.Username == creds[0] && user.Password == creds[1] {
match = true match = true
userId = user.UserID userId = user.UserID
@ -84,29 +85,29 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
} }
jfId := "" jfId := ""
if !match { if !match {
if !ctx.jellyfinLogin { if !app.jellyfinLogin {
ctx.info.Println("Auth failed: Invalid username and/or password") app.info.Println("Auth failed: Invalid username and/or password")
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
var status int var status int
var err error var err error
var user map[string]interface{} var user map[string]interface{}
user, status, err = ctx.authJf.authenticate(creds[0], creds[1]) user, status, err = app.authJf.authenticate(creds[0], creds[1])
jfId = user["Id"].(string) jfId = user["Id"].(string)
if status != 200 || err != nil { if status != 200 || err != nil {
if status == 401 { if status == 401 {
ctx.info.Println("Auth failed: Invalid username and/or password") app.info.Println("Auth failed: Invalid username and/or password")
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
ctx.err.Printf("Auth failed: Couldn't authenticate with Jellyfin: Code %d", status) app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin: Code %d", status)
respond(500, "Jellyfin error", gc) respond(500, "Jellyfin error", gc)
return return
} else { } else {
if ctx.config.Section("ui").Key("admin_only").MustBool(true) { if app.config.Section("ui").Key("admin_only").MustBool(true) {
if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) { if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) {
ctx.debug.Printf("Auth failed: User \"%s\" isn't admin", creds[0]) app.debug.Printf("Auth failed: User \"%s\" isn't admin", creds[0])
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
} }
} }
@ -114,8 +115,8 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
newuser.UserID = shortuuid.New() newuser.UserID = shortuuid.New()
userId = newuser.UserID userId = newuser.UserID
// uuid, nothing else identifiable! // uuid, nothing else identifiable!
ctx.debug.Printf("Token generated for user \"%s\"", creds[0]) app.debug.Printf("Token generated for user \"%s\"", creds[0])
ctx.users = append(ctx.users, newuser) app.users = append(app.users, newuser)
} }
} }
token, err := CreateToken(userId, jfId) token, err := CreateToken(userId, jfId)

@ -1,9 +1,10 @@
package main package main
import ( import (
"gopkg.in/ini.v1"
"path/filepath" "path/filepath"
"strconv" "strconv"
"gopkg.in/ini.v1"
) )
/*var DeCamel ini.NameMapper = func(raw string) string { /*var DeCamel ini.NameMapper = func(raw string) string {
@ -22,51 +23,51 @@ import (
return string(out) return string(out)
} }
func (ctx *appContext) loadDefaults() (err error) { func (app *appContext) loadDefaults() (err error) {
var cfb []byte var cfb []byte
cfb, err = ioutil.ReadFile(ctx.configBase_path) cfb, err = ioutil.ReadFile(app.configBase_path)
if err != nil { if err != nil {
return return
} }
json.Unmarshal(cfb, ctx.defaults) json.Unmarshal(cfb, app.defaults)
return return
}*/ }*/
func (ctx *appContext) loadConfig() error { func (app *appContext) loadConfig() error {
var err error var err error
ctx.config, err = ini.Load(ctx.config_path) app.config, err = ini.Load(app.config_path)
if err != nil { if err != nil {
return err return err
} }
ctx.config.Section("jellyfin").Key("public_server").SetValue(ctx.config.Section("jellyfin").Key("public_server").MustString(ctx.config.Section("jellyfin").Key("server").String())) app.config.Section("jellyfin").Key("public_server").SetValue(app.config.Section("jellyfin").Key("public_server").MustString(app.config.Section("jellyfin").Key("server").String()))
for _, key := range ctx.config.Section("files").Keys() { for _, key := range app.config.Section("files").Keys() {
// if key.MustString("") == "" && key.Name() != "custom_css" { // if key.MustString("") == "" && key.Name() != "custom_css" {
// key.SetValue(filepath.Join(ctx.data_path, (key.Name() + ".json"))) // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
// } // }
key.SetValue(key.MustString(filepath.Join(ctx.data_path, (key.Name() + ".json")))) key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json"))))
} }
for _, key := range []string{"user_configuration", "user_displayprefs"} { for _, key := range []string{"user_configuration", "user_displayprefs"} {
// if ctx.config.Section("files").Key(key).MustString("") == "" { // if app.config.Section("files").Key(key).MustString("") == "" {
// key.SetValue(filepath.Join(ctx.data_path, (key.Name() + ".json"))) // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
// } // }
ctx.config.Section("files").Key(key).SetValue(ctx.config.Section("files").Key(key).MustString(filepath.Join(ctx.data_path, (key + ".json")))) app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.data_path, (key + ".json"))))
} }
ctx.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(ctx.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)))
ctx.config.Section("password_resets").Key("email_html").SetValue(ctx.config.Section("password_resets").Key("email_html").MustString(filepath.Join(ctx.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.local_path, "email.html")))
ctx.config.Section("password_resets").Key("email_text").SetValue(ctx.config.Section("password_resets").Key("email_text").MustString(filepath.Join(ctx.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.local_path, "email.txt")))
ctx.config.Section("invite_emails").Key("email_html").SetValue(ctx.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(ctx.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.local_path, "invite-email.html")))
ctx.config.Section("invite_emails").Key("email_text").SetValue(ctx.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(ctx.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.local_path, "invite-email.txt")))
ctx.config.Section("notifications").Key("expiry_html").SetValue(ctx.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(ctx.local_path, "expired.html"))) app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.local_path, "expired.html")))
ctx.config.Section("notifications").Key("expiry_text").SetValue(ctx.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(ctx.local_path, "expired.txt"))) app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.local_path, "expired.txt")))
ctx.config.Section("notifications").Key("created_html").SetValue(ctx.config.Section("notifications").Key("created_html").MustString(filepath.Join(ctx.local_path, "created.html"))) app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.local_path, "created.html")))
ctx.config.Section("notifications").Key("created_text").SetValue(ctx.config.Section("notifications").Key("created_text").MustString(filepath.Join(ctx.local_path, "created.txt"))) app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.local_path, "created.txt")))
return nil return nil
} }

@ -9,21 +9,21 @@ type Repeater struct {
ShutdownChannel chan string ShutdownChannel chan string
Interval time.Duration Interval time.Duration
period time.Duration period time.Duration
ctx *appContext app *appContext
} }
func NewRepeater(interval time.Duration, ctx *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,
period: interval, period: interval,
ctx: ctx, app: app,
} }
} }
func (rt *Repeater) Run() { func (rt *Repeater) Run() {
rt.ctx.info.Println("Invite daemon started") rt.app.info.Println("Invite daemon started")
for { for {
select { select {
case <-rt.ShutdownChannel: case <-rt.ShutdownChannel:
@ -33,9 +33,9 @@ func (rt *Repeater) Run() {
break break
} }
started := time.Now() started := time.Now()
rt.ctx.storage.loadInvites() rt.app.storage.loadInvites()
rt.ctx.debug.Println("Daemon: Checking invites") rt.app.debug.Println("Daemon: Checking invites")
rt.ctx.checkInvites() rt.app.checkInvites()
finished := time.Now() finished := time.Now()
duration := finished.Sub(started) duration := finished.Sub(started)
rt.period = rt.Interval - duration rt.period = rt.Interval - duration

@ -5,13 +5,14 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
jEmail "github.com/jordan-wright/email"
"github.com/knz/strtime"
"github.com/mailgun/mailgun-go/v4"
"html/template" "html/template"
"net/smtp" "net/smtp"
"strings" "strings"
"time" "time"
jEmail "github.com/jordan-wright/email"
"github.com/knz/strtime"
"github.com/mailgun/mailgun-go/v4"
) )
type Emailer struct { type Emailer struct {
@ -49,13 +50,13 @@ func (email *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern,
return return
} }
func (email *Emailer) init(ctx *appContext) { func (email *Emailer) init(app *appContext) {
email.fromAddr = ctx.config.Section("email").Key("address").String() email.fromAddr = app.config.Section("email").Key("address").String()
email.fromName = ctx.config.Section("email").Key("from").String() email.fromName = app.config.Section("email").Key("from").String()
email.sendMethod = ctx.config.Section("email").Key("method").String() email.sendMethod = app.config.Section("email").Key("method").String()
if email.sendMethod == "mailgun" { if email.sendMethod == "mailgun" {
email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], ctx.config.Section("mailgun").Key("api_key").String()) email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], app.config.Section("mailgun").Key("api_key").String())
api_url := ctx.config.Section("mailgun").Key("api_url").String() api_url := app.config.Section("mailgun").Key("api_url").String()
// Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages' // Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages'
if strings.Contains(api_url, "messages") { if strings.Contains(api_url, "messages") {
api_url = api_url[0:strings.LastIndex(api_url, "/")] api_url = api_url[0:strings.LastIndex(api_url, "/")]
@ -63,21 +64,21 @@ func (email *Emailer) init(ctx *appContext) {
} }
email.mg.SetAPIBase(api_url) email.mg.SetAPIBase(api_url)
} else if email.sendMethod == "smtp" { } else if email.sendMethod == "smtp" {
ctx.host = ctx.config.Section("smtp").Key("server").String() app.host = app.config.Section("smtp").Key("server").String()
email.smtpAuth = smtp.PlainAuth("", email.fromAddr, ctx.config.Section("smtp").Key("password").String(), ctx.host) email.smtpAuth = smtp.PlainAuth("", email.fromAddr, app.config.Section("smtp").Key("password").String(), app.host)
} }
} }
func (email *Emailer) constructInvite(code string, invite Invite, ctx *appContext) error { func (email *Emailer) constructInvite(code string, invite Invite, app *appContext) error {
email.content.subject = ctx.config.Section("invite_emails").Key("subject").String() email.content.subject = app.config.Section("invite_emails").Key("subject").String()
expiry := invite.ValidTill expiry := invite.ValidTill
d, t, expires_in := email.formatExpiry(expiry, false, ctx.datePattern, ctx.timePattern) d, t, expires_in := email.formatExpiry(expiry, false, app.datePattern, app.timePattern)
message := ctx.config.Section("email").Key("message").String() message := app.config.Section("email").Key("message").String()
invite_link := ctx.config.Section("invite_emails").Key("url_base").String() invite_link := app.config.Section("invite_emails").Key("url_base").String()
invite_link = fmt.Sprintf("%s/%s", invite_link, code) invite_link = fmt.Sprintf("%s/%s", invite_link, code)
for _, key := range []string{"html", "text"} { for _, key := range []string{"html", "text"} {
fpath := ctx.config.Section("invite_emails").Key("email_" + key).String() fpath := app.config.Section("invite_emails").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath) tpl, err := template.ParseFiles(fpath)
if err != nil { if err != nil {
return err return err
@ -103,11 +104,11 @@ func (email *Emailer) constructInvite(code string, invite Invite, ctx *appContex
return nil return nil
} }
func (email *Emailer) constructExpiry(code string, invite Invite, ctx *appContext) error { func (email *Emailer) constructExpiry(code string, invite Invite, app *appContext) error {
email.content.subject = "Notice: Invite expired" email.content.subject = "Notice: Invite expired"
expiry := ctx.formatDatetime(invite.ValidTill) expiry := app.formatDatetime(invite.ValidTill)
for _, key := range []string{"html", "text"} { for _, key := range []string{"html", "text"} {
fpath := ctx.config.Section("notifications").Key("expiry_" + key).String() fpath := app.config.Section("notifications").Key("expiry_" + key).String()
tpl, err := template.ParseFiles(fpath) tpl, err := template.ParseFiles(fpath)
if err != nil { if err != nil {
return err return err
@ -130,17 +131,17 @@ func (email *Emailer) constructExpiry(code string, invite Invite, ctx *appContex
return nil return nil
} }
func (email *Emailer) constructCreated(code, username, address string, invite Invite, ctx *appContext) error { func (email *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) error {
email.content.subject = "Notice: User created" email.content.subject = "Notice: User created"
created := ctx.formatDatetime(invite.Created) created := app.formatDatetime(invite.Created)
var tplAddress string var tplAddress string
if ctx.config.Section("email").Key("no_username").MustBool(false) { if app.config.Section("email").Key("no_username").MustBool(false) {
tplAddress = "n/a" tplAddress = "n/a"
} else { } else {
tplAddress = address tplAddress = address
} }
for _, key := range []string{"html", "text"} { for _, key := range []string{"html", "text"} {
fpath := ctx.config.Section("notifications").Key("created_" + key).String() fpath := app.config.Section("notifications").Key("created_" + key).String()
tpl, err := template.ParseFiles(fpath) tpl, err := template.ParseFiles(fpath)
if err != nil { if err != nil {
return err return err
@ -165,12 +166,12 @@ func (email *Emailer) constructCreated(code, username, address string, invite In
return nil return nil
} }
func (email *Emailer) constructReset(pwr Pwr, ctx *appContext) error { func (email *Emailer) constructReset(pwr Pwr, app *appContext) error {
email.content.subject = ctx.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin") email.content.subject = app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin")
d, t, expires_in := email.formatExpiry(pwr.Expiry, true, ctx.datePattern, ctx.timePattern) d, t, expires_in := email.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
message := ctx.config.Section("email").Key("message").String() message := app.config.Section("email").Key("message").String()
for _, key := range []string{"html", "text"} { for _, key := range []string{"html", "text"} {
fpath := ctx.config.Section("password_resets").Key("email_" + key).String() fpath := app.config.Section("password_resets").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath) tpl, err := template.ParseFiles(fpath)
if err != nil { if err != nil {
return err return err
@ -197,7 +198,7 @@ func (email *Emailer) constructReset(pwr Pwr, ctx *appContext) error {
return nil return nil
} }
func (email *Emailer) send(address string, ctx *appContext) error { func (email *Emailer) send(address string, app *appContext) error {
if email.sendMethod == "mailgun" { if email.sendMethod == "mailgun" {
message := email.mg.NewMessage( message := email.mg.NewMessage(
fmt.Sprintf("%s <%s>", email.fromName, email.fromAddr), fmt.Sprintf("%s <%s>", email.fromName, email.fromAddr),
@ -205,9 +206,9 @@ func (email *Emailer) send(address string, ctx *appContext) error {
email.content.text, email.content.text,
address) address)
message.SetHtml(email.content.html) message.SetHtml(email.content.html)
mgctx, cancel := context.WithTimeout(context.Background(), time.Second*30) mgapp, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel() defer cancel()
_, _, err := email.mg.Send(mgctx, message) _, _, err := email.mg.Send(mgapp, message)
if err != nil { if err != nil {
return err return err
} }
@ -218,19 +219,19 @@ func (email *Emailer) send(address string, ctx *appContext) error {
e.To = []string{address} e.To = []string{address}
e.Text = []byte(email.content.text) e.Text = []byte(email.content.text)
e.HTML = []byte(email.content.html) e.HTML = []byte(email.content.html)
smtpType := ctx.config.Section("smtp").Key("encryption").String() smtpType := app.config.Section("smtp").Key("encryption").String()
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
InsecureSkipVerify: false, InsecureSkipVerify: false,
ServerName: ctx.host, ServerName: app.host,
} }
var err error var err error
if smtpType == "ssl_tls" { if smtpType == "ssl_tls" {
port := ctx.config.Section("smtp").Key("port").MustInt(465) port := app.config.Section("smtp").Key("port").MustInt(465)
server := fmt.Sprintf("%s:%d", ctx.host, port) server := fmt.Sprintf("%s:%d", app.host, port)
err = e.SendWithTLS(server, email.smtpAuth, tlsConfig) err = e.SendWithTLS(server, email.smtpAuth, tlsConfig)
} else if smtpType == "starttls" { } else if smtpType == "starttls" {
port := ctx.config.Section("smtp").Key("port").MustInt(587) port := app.config.Section("smtp").Key("port").MustInt(587)
server := fmt.Sprintf("%s:%d", ctx.host, port) server := fmt.Sprintf("%s:%d", app.host, port)
e.SendWithStartTLS(server, email.smtpAuth, tlsConfig) e.SendWithStartTLS(server, email.smtpAuth, tlsConfig)
} }
return err return err

@ -7,11 +7,6 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"github.com/gin-contrib/pprof"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@ -20,6 +15,12 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"time" "time"
"github.com/gin-contrib/pprof"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
) )
// Username is JWT! // Username is JWT!
@ -95,190 +96,190 @@ func setGinLogger(router *gin.Engine, debugMode bool) {
} }
func main() { func main() {
ctx := new(appContext) app := new(appContext)
userConfigDir, _ := os.UserConfigDir() userConfigDir, _ := os.UserConfigDir()
ctx.data_path = filepath.Join(userConfigDir, "jfa-go") app.data_path = filepath.Join(userConfigDir, "jfa-go")
ctx.config_path = filepath.Join(ctx.data_path, "config.ini") app.config_path = filepath.Join(app.data_path, "config.ini")
executable, _ := os.Executable() executable, _ := os.Executable()
ctx.local_path = filepath.Join(filepath.Dir(executable), "data") app.local_path = filepath.Join(filepath.Dir(executable), "data")
ctx.info = log.New(os.Stdout, "[INFO] ", log.Ltime) app.info = log.New(os.Stdout, "[INFO] ", log.Ltime)
ctx.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile) app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile)
dataPath := flag.String("data", ctx.data_path, "alternate path to data directory.") dataPath := flag.String("data", app.data_path, "alternate path to data directory.")
configPath := flag.String("config", ctx.config_path, "alternate path to config file.") configPath := flag.String("config", app.config_path, "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.")
flag.Parse() flag.Parse()
if ctx.config_path == *configPath && ctx.data_path != *dataPath { if app.config_path == *configPath && app.data_path != *dataPath {
ctx.config_path = filepath.Join(*dataPath, "config.ini") app.config_path = filepath.Join(*dataPath, "config.ini")
} else { } else {
ctx.config_path = *configPath app.config_path = *configPath
ctx.data_path = *dataPath app.data_path = *dataPath
} }
// 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 != "" {
ctx.config_path = v app.config_path = v
} }
if v := os.Getenv("JFA_DATAPATH"); v != "" { if v := os.Getenv("JFA_DATAPATH"); v != "" {
ctx.data_path = v app.data_path = v
} }
os.Setenv("JFA_CONFIGPATH", ctx.config_path) os.Setenv("JFA_CONFIGPATH", app.config_path)
os.Setenv("JFA_DATAPATH", ctx.data_path) os.Setenv("JFA_DATAPATH", app.data_path)
var firstRun bool var firstRun bool
if _, err := os.Stat(ctx.data_path); os.IsNotExist(err) { if _, err := os.Stat(app.data_path); os.IsNotExist(err) {
os.Mkdir(ctx.data_path, 0700) os.Mkdir(app.data_path, 0700)
} }
if _, err := os.Stat(ctx.config_path); os.IsNotExist(err) { if _, err := os.Stat(app.config_path); os.IsNotExist(err) {
firstRun = true firstRun = true
dConfigPath := filepath.Join(ctx.local_path, "config-default.ini") dConfigPath := filepath.Join(app.local_path, "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 {
ctx.err.Fatalf("Couldn't find default config file \"%s\"", dConfigPath) app.err.Fatalf("Couldn't find default config file \"%s\"", dConfigPath)
} }
defer dConfig.Close() defer dConfig.Close()
var nConfig *os.File var nConfig *os.File
nConfig, err := os.Create(ctx.config_path) nConfig, err := os.Create(app.config_path)
if err != nil { if err != nil {
ctx.err.Fatalf("Couldn't open config file for writing: \"%s\"", dConfigPath) app.err.Fatalf("Couldn't open config file for writing: \"%s\"", dConfigPath)
} }
defer nConfig.Close() defer nConfig.Close()
_, err = io.Copy(nConfig, dConfig) _, err = io.Copy(nConfig, dConfig)
if err != nil { if err != nil {
ctx.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, ctx.config_path) app.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, app.config_path)
} }
ctx.info.Printf("Copied default configuration to \"%s\"", ctx.config_path) app.info.Printf("Copied default configuration to \"%s\"", app.config_path)
} }
var debugMode bool var debugMode bool
var address string var address string
if ctx.loadConfig() != nil { if app.loadConfig() != nil {
ctx.err.Fatalf("Failed to load config file \"%s\"", ctx.config_path) app.err.Fatalf("Failed to load config file \"%s\"", app.config_path)
} }
ctx.version = ctx.config.Section("jellyfin").Key("version").String() app.version = app.config.Section("jellyfin").Key("version").String()
debugMode = ctx.config.Section("ui").Key("debug").MustBool(true) debugMode = app.config.Section("ui").Key("debug").MustBool(true)
if debugMode { if debugMode {
ctx.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile) app.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile)
} else { } else {
ctx.debug = log.New(ioutil.Discard, "", 0) app.debug = log.New(ioutil.Discard, "", 0)
} }
if !firstRun { if !firstRun {
ctx.host = ctx.config.Section("ui").Key("host").String() app.host = app.config.Section("ui").Key("host").String()
ctx.port = ctx.config.Section("ui").Key("port").MustInt(8056) app.port = app.config.Section("ui").Key("port").MustInt(8056)
if *host != ctx.host && *host != "" { if *host != app.host && *host != "" {
ctx.host = *host app.host = *host
} }
if *port != ctx.port && *port > 0 { if *port != app.port && *port > 0 {
ctx.port = *port app.port = *port
} }
if h := os.Getenv("JFA_HOST"); h != "" { if h := os.Getenv("JFA_HOST"); h != "" {
ctx.host = h app.host = h
if p := os.Getenv("JFA_PORT"); p != "" { if p := os.Getenv("JFA_PORT"); p != "" {
var port int var port int
_, err := fmt.Sscan(p, &port) _, err := fmt.Sscan(p, &port)
if err == nil { if err == nil {
ctx.port = port app.port = port
} }
} }
} }
address = fmt.Sprintf("%s:%d", ctx.host, ctx.port) address = fmt.Sprintf("%s:%d", app.host, app.port)
ctx.debug.Printf("Loaded config file \"%s\"", ctx.config_path) app.debug.Printf("Loaded config file \"%s\"", app.config_path)
if ctx.config.Section("ui").Key("bs5").MustBool(false) { if app.config.Section("ui").Key("bs5").MustBool(false) {
ctx.cssFile = "bs5-jf.css" app.cssFile = "bs5-jf.css"
ctx.bsVersion = 5 app.bsVersion = 5
} else { } else {
ctx.cssFile = "bs4-jf.css" app.cssFile = "bs4-jf.css"
ctx.bsVersion = 4 app.bsVersion = 4
} }
ctx.debug.Println("Loading storage") app.debug.Println("Loading storage")
ctx.storage.invite_path = filepath.Join(ctx.data_path, "invites.json") app.storage.invite_path = filepath.Join(app.data_path, "invites.json")
ctx.storage.loadInvites() app.storage.loadInvites()
ctx.storage.emails_path = filepath.Join(ctx.data_path, "emails.json") app.storage.emails_path = filepath.Join(app.data_path, "emails.json")
ctx.storage.loadEmails() app.storage.loadEmails()
ctx.storage.policy_path = filepath.Join(ctx.data_path, "user_template.json") app.storage.policy_path = filepath.Join(app.data_path, "user_template.json")
ctx.storage.loadPolicy() app.storage.loadPolicy()
ctx.storage.configuration_path = filepath.Join(ctx.data_path, "user_configuration.json") app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
ctx.storage.loadConfiguration() app.storage.loadConfiguration()
ctx.storage.displayprefs_path = filepath.Join(ctx.data_path, "user_displayprefs.json") app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
ctx.storage.loadDisplayprefs() app.storage.loadDisplayprefs()
ctx.configBase_path = filepath.Join(ctx.local_path, "config-base.json") app.configBase_path = filepath.Join(app.local_path, "config-base.json")
config_base, _ := ioutil.ReadFile(ctx.configBase_path) config_base, _ := ioutil.ReadFile(app.configBase_path)
json.Unmarshal(config_base, &ctx.configBase) json.Unmarshal(config_base, &app.configBase)
themes := map[string]string{ themes := map[string]string{
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", ctx.bsVersion), "Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", app.bsVersion),
"Bootstrap (Light)": fmt.Sprintf("bs%d.css", ctx.bsVersion), "Bootstrap (Light)": fmt.Sprintf("bs%d.css", app.bsVersion),
"Custom CSS": "", "Custom CSS": "",
} }
if val, ok := themes[ctx.config.Section("ui").Key("theme").String()]; ok { if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok {
ctx.cssFile = val app.cssFile = val
} }
ctx.debug.Printf("Using css file \"%s\"", ctx.cssFile) app.debug.Printf("Using css file \"%s\"", app.cssFile)
secret, err := GenerateSecret(16) secret, err := GenerateSecret(16)
if err != nil { if err != nil {
ctx.err.Fatal(err) app.err.Fatal(err)
} }
os.Setenv("JFA_SECRET", secret) os.Setenv("JFA_SECRET", secret)
ctx.jellyfinLogin = true app.jellyfinLogin = true
if val, _ := ctx.config.Section("ui").Key("jellyfin_login").Bool(); !val { if val, _ := app.config.Section("ui").Key("jellyfin_login").Bool(); !val {
ctx.jellyfinLogin = false app.jellyfinLogin = false
user := User{} user := User{}
user.UserID = shortuuid.New() user.UserID = shortuuid.New()
user.Username = ctx.config.Section("ui").Key("username").String() user.Username = app.config.Section("ui").Key("username").String()
user.Password = ctx.config.Section("ui").Key("password").String() user.Password = app.config.Section("ui").Key("password").String()
ctx.users = append(ctx.users, user) app.users = append(app.users, user)
} else { } else {
ctx.debug.Println("Using Jellyfin for authentication") app.debug.Println("Using Jellyfin for authentication")
} }
server := ctx.config.Section("jellyfin").Key("server").String() server := app.config.Section("jellyfin").Key("server").String()
ctx.jf.init(server, "jfa-go", ctx.version, "hrfee-arch", "hrfee-arch") app.jf.init(server, "jfa-go", app.version, "hrfee-arch", "hrfee-arch")
var status int var status int
_, status, err = ctx.jf.authenticate(ctx.config.Section("jellyfin").Key("username").String(), ctx.config.Section("jellyfin").Key("password").String()) _, status, err = app.jf.authenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String())
if status != 200 || err != nil { if status != 200 || err != nil {
ctx.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status) app.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
} }
ctx.info.Printf("Authenticated with %s", server) app.info.Printf("Authenticated with %s", server)
ctx.authJf.init(server, "jfa-go", ctx.version, "auth", "auth") app.authJf.init(server, "jfa-go", app.version, "auth", "auth")
ctx.loadStrftime() app.loadStrftime()
validatorConf := ValidatorConf{ validatorConf := ValidatorConf{
"characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0), "characters": app.config.Section("password_validation").Key("min_length").MustInt(0),
"uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0), "uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0),
"lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0), "lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0),
"numbers": ctx.config.Section("password_validation").Key("number").MustInt(0), "numbers": app.config.Section("password_validation").Key("number").MustInt(0),
"special characters": ctx.config.Section("password_validation").Key("special").MustInt(0), "special characters": app.config.Section("password_validation").Key("special").MustInt(0),
} }
if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) { if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
for key := range validatorConf { for key := range validatorConf {
validatorConf[key] = 0 validatorConf[key] = 0
} }
} }
ctx.validator.init(validatorConf) app.validator.init(validatorConf)
ctx.email.init(ctx) app.email.init(app)
inviteDaemon := NewRepeater(time.Duration(60*time.Second), ctx) inviteDaemon := NewRepeater(time.Duration(60*time.Second), app)
go inviteDaemon.Run() go inviteDaemon.Run()
if ctx.config.Section("password_resets").Key("enabled").MustBool(false) { if app.config.Section("password_resets").Key("enabled").MustBool(false) {
go ctx.StartPWR() go app.StartPWR()
} }
} else { } else {
debugMode = false debugMode = false
@ -286,43 +287,43 @@ func main() {
address = "0.0.0.0:8056" address = "0.0.0.0:8056"
} }
ctx.info.Println("Loading routes") app.info.Println("Loading routes")
router := gin.New() router := gin.New()
setGinLogger(router, debugMode) setGinLogger(router, debugMode)
router.Use(gin.Recovery()) router.Use(gin.Recovery())
router.Use(static.Serve("/", static.LocalFile(filepath.Join(ctx.local_path, "static"), false))) router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
router.LoadHTMLGlob(filepath.Join(ctx.local_path, "templates", "*")) router.LoadHTMLGlob(filepath.Join(app.local_path, "templates", "*"))
router.NoRoute(ctx.NoRouteHandler) router.NoRoute(app.NoRouteHandler)
if debugMode { if debugMode {
ctx.debug.Println("Loading pprof") app.debug.Println("Loading pprof")
pprof.Register(router) pprof.Register(router)
} }
if !firstRun { if !firstRun {
router.GET("/", ctx.AdminPage) router.GET("/", app.AdminPage)
router.GET("/getToken", ctx.GetToken) router.GET("/getToken", app.GetToken)
router.POST("/newUser", ctx.NewUser) router.POST("/newUser", app.NewUser)
router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(ctx.local_path, "static"), false))) router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
router.GET("/invite/:invCode", ctx.InviteProxy) router.GET("/invite/:invCode", app.InviteProxy)
api := router.Group("/", ctx.webAuth()) api := router.Group("/", app.webAuth())
api.POST("/generateInvite", ctx.GenerateInvite) api.POST("/generateInvite", app.GenerateInvite)
api.GET("/getInvites", ctx.GetInvites) api.GET("/getInvites", app.GetInvites)
api.POST("/setNotify", ctx.SetNotify) api.POST("/setNotify", app.SetNotify)
api.POST("/deleteInvite", ctx.DeleteInvite) api.POST("/deleteInvite", app.DeleteInvite)
api.GET("/getUsers", ctx.GetUsers) api.GET("/getUsers", app.GetUsers)
api.POST("/modifyUsers", ctx.ModifyEmails) api.POST("/modifyUsers", app.ModifyEmails)
api.POST("/setDefaults", ctx.SetDefaults) api.POST("/setDefaults", app.SetDefaults)
api.GET("/getConfig", ctx.GetConfig) api.GET("/getConfig", app.GetConfig)
api.POST("/modifyConfig", ctx.ModifyConfig) api.POST("/modifyConfig", app.ModifyConfig)
ctx.info.Printf("Starting router @ %s", address) app.info.Printf("Starting router @ %s", address)
} else { } else {
router.GET("/", func(gc *gin.Context) { router.GET("/", func(gc *gin.Context) {
gc.HTML(200, "setup.html", gin.H{}) gc.HTML(200, "setup.html", gin.H{})
}) })
router.POST("/testJF", ctx.TestJF) router.POST("/testJF", app.TestJF)
router.POST("/modifyConfig", ctx.ModifyConfig) router.POST("/modifyConfig", app.ModifyConfig)
ctx.info.Printf("Loading setup @ %s", address) app.info.Printf("Loading setup @ %s", address)
} }
srv := &http.Server{ srv := &http.Server{
@ -331,17 +332,17 @@ func main() {
} }
go func() { go func() {
if err := srv.ListenAndServe(); err != nil { if err := srv.ListenAndServe(); err != nil {
ctx.err.Printf("Failure serving: %s", err) app.err.Printf("Failure serving: %s", err)
} }
}() }()
ctx.quit = make(chan os.Signal) app.quit = make(chan os.Signal)
signal.Notify(ctx.quit, os.Interrupt) signal.Notify(app.quit, os.Interrupt)
<-ctx.quit <-app.quit
ctx.info.Println("Shutting down...") app.info.Println("Shutting down...")
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5) cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel() defer cancel()
if err := srv.Shutdown(cntx); err != nil { if err := srv.Shutdown(cntx); err != nil {
ctx.err.Fatalf("Server shutdown error: %s", err) app.err.Fatalf("Server shutdown error: %s", err)
} }
} }

@ -2,33 +2,34 @@ package main
import ( import (
"encoding/json" "encoding/json"
"github.com/fsnotify/fsnotify"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/fsnotify/fsnotify"
) )
func (ctx *appContext) StartPWR() { func (app *appContext) StartPWR() {
ctx.info.Println("Starting password reset daemon") app.info.Println("Starting password reset daemon")
path := ctx.config.Section("password_resets").Key("watch_directory").String() path := app.config.Section("password_resets").Key("watch_directory").String()
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
ctx.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path) app.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path)
return return
} }
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
ctx.err.Printf("Couldn't initialise password reset daemon") app.err.Printf("Couldn't initialise password reset daemon")
return return
} }
defer watcher.Close() defer watcher.Close()
done := make(chan bool) done := make(chan bool)
go pwrMonitor(ctx, watcher) go pwrMonitor(app, watcher)
err = watcher.Add(path) err = watcher.Add(path)
if err != nil { if err != nil {
ctx.err.Printf("Failed to start password reset daemon: %s", err) app.err.Printf("Failed to start password reset daemon: %s", err)
} }
<-done <-done
} }
@ -39,7 +40,7 @@ type Pwr struct {
Expiry time.Time `json:"ExpirationDate"` Expiry time.Time `json:"ExpirationDate"`
} }
func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) { func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
for { for {
select { select {
case event, ok := <-watcher.Events: case event, ok := <-watcher.Events:
@ -56,29 +57,29 @@ func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) {
if len(pwr.Pin) == 0 || err != nil { if len(pwr.Pin) == 0 || err != nil {
return return
} }
ctx.info.Printf("New password reset for user \"%s\"", pwr.Username) app.info.Printf("New password reset for user \"%s\"", pwr.Username)
if ct := time.Now(); pwr.Expiry.After(ct) { if ct := time.Now(); pwr.Expiry.After(ct) {
user, status, err := ctx.jf.userByName(pwr.Username, false) user, status, err := app.jf.userByName(pwr.Username, false)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status) app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
ctx.debug.Printf("Error: %s", err) app.debug.Printf("Error: %s", err)
return return
} }
ctx.storage.loadEmails() app.storage.loadEmails()
address, ok := ctx.storage.emails[user["Id"].(string)].(string) address, ok := app.storage.emails[user["Id"].(string)].(string)
if !ok { if !ok {
ctx.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username) app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
return return
} }
if ctx.email.constructReset(pwr, ctx) != nil { if app.email.constructReset(pwr, app) != nil {
ctx.err.Printf("Failed to construct password reset email for %s", pwr.Username) app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
} else if ctx.email.send(address, ctx) != nil { } else if app.email.send(address, app) != nil {
ctx.err.Printf("Failed to send password reset email to \"%s\"", address) app.err.Printf("Failed to send password reset email to \"%s\"", address)
} else { } else {
ctx.info.Printf("Sent password reset email to \"%s\"", address) app.info.Printf("Sent password reset email to \"%s\"", address)
} }
} else { } else {
ctx.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry) app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)
} }
} }
@ -86,7 +87,7 @@ func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) {
if !ok { if !ok {
return return
} }
ctx.err.Printf("Password reset daemon: %s", err) app.err.Printf("Password reset daemon: %s", err)
} }
} }
} }

@ -10,14 +10,14 @@ type testReq struct {
Password string `json:"jfPassword"` Password string `json:"jfPassword"`
} }
func (ctx *appContext) TestJF(gc *gin.Context) { func (app *appContext) TestJF(gc *gin.Context) {
var req testReq var req testReq
gc.BindJSON(&req) gc.BindJSON(&req)
tempjf := Jellyfin{} tempjf := Jellyfin{}
tempjf.init(req.Host, "jfa-go-setup", ctx.version, "auth", "auth") tempjf.init(req.Host, "jfa-go-setup", app.version, "auth", "auth")
_, status, err := tempjf.authenticate(req.Username, req.Password) _, status, err := tempjf.authenticate(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
ctx.info.Printf("Auth failed with code %d (%s)", status, err) app.info.Printf("Auth failed with code %d (%s)", status, err)
gc.JSON(401, map[string]bool{"success": false}) gc.JSON(401, map[string]bool{"success": false})
return return
} }

@ -1,54 +1,55 @@
package main package main
import ( import (
"github.com/gin-gonic/gin"
"net/http" "net/http"
"github.com/gin-gonic/gin"
) )
func (ctx *appContext) AdminPage(gc *gin.Context) { func (app *appContext) AdminPage(gc *gin.Context) {
bs5 := ctx.config.Section("ui").Key("bs5").MustBool(false) bs5 := app.config.Section("ui").Key("bs5").MustBool(false)
emailEnabled, _ := ctx.config.Section("invite_emails").Key("enabled").Bool() emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := ctx.config.Section("notifications").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
gc.HTML(http.StatusOK, "admin.html", gin.H{ gc.HTML(http.StatusOK, "admin.html", gin.H{
"bs5": bs5, "bs5": bs5,
"cssFile": ctx.cssFile, "cssFile": app.cssFile,
"contactMessage": "", "contactMessage": "",
"email_enabled": emailEnabled, "email_enabled": emailEnabled,
"notifications": notificationsEnabled, "notifications": notificationsEnabled,
}) })
} }
func (ctx *appContext) InviteProxy(gc *gin.Context) { func (app *appContext) InviteProxy(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */ /* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
// if ctx.checkInvite(code, false, "") { // if app.checkInvite(code, false, "") {
if _, ok := ctx.storage.invites[code]; ok { if _, ok := app.storage.invites[code]; ok {
email := ctx.storage.invites[code].Email email := app.storage.invites[code].Email
gc.HTML(http.StatusOK, "form.html", gin.H{ gc.HTML(http.StatusOK, "form.html", gin.H{
"bs5": ctx.config.Section("ui").Key("bs5").MustBool(false), "bs5": app.config.Section("ui").Key("bs5").MustBool(false),
"cssFile": ctx.cssFile, "cssFile": app.cssFile,
"contactMessage": ctx.config.Section("ui").Key("contac_message").String(), "contactMessage": app.config.Section("ui").Key("contac_message").String(),
"helpMessage": ctx.config.Section("ui").Key("help_message").String(), "helpMessage": app.config.Section("ui").Key("help_message").String(),
"successMessage": ctx.config.Section("ui").Key("success_message").String(), "successMessage": app.config.Section("ui").Key("success_message").String(),
"jfLink": ctx.config.Section("jellyfin").Key("public_server").String(), "jfLink": app.config.Section("jellyfin").Key("public_server").String(),
"validate": ctx.config.Section("password_validation").Key("enabled").MustBool(false), "validate": app.config.Section("password_validation").Key("enabled").MustBool(false),
"requirements": ctx.validator.getCriteria(), "requirements": app.validator.getCriteria(),
"email": email, "email": email,
"username": !ctx.config.Section("email").Key("no_username").MustBool(false), "username": !app.config.Section("email").Key("no_username").MustBool(false),
}) })
} else { } else {
gc.HTML(404, "invalidCode.html", gin.H{ gc.HTML(404, "invalidCode.html", gin.H{
"bs5": ctx.config.Section("ui").Key("bs5").MustBool(false), "bs5": app.config.Section("ui").Key("bs5").MustBool(false),
"cssFile": ctx.cssFile, "cssFile": app.cssFile,
"contactMessage": ctx.config.Section("ui").Key("contac_message").String(), "contactMessage": app.config.Section("ui").Key("contac_message").String(),
}) })
} }
} }
func (ctx *appContext) NoRouteHandler(gc *gin.Context) { func (app *appContext) NoRouteHandler(gc *gin.Context) {
gc.HTML(404, "404.html", gin.H{ gc.HTML(404, "404.html", gin.H{
"bs5": ctx.config.Section("ui").Key("bs5").MustBool(false), "bs5": app.config.Section("ui").Key("bs5").MustBool(false),
"cssFile": ctx.cssFile, "cssFile": app.cssFile,
"contactMessage": ctx.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
}) })
} }

Loading…
Cancel
Save