mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
use app identifier instead of ctx
changing this because ctx is commonly used with the context package.
This commit is contained in:
parent
fffb3471d6
commit
fd766e7b1a
349
api.go
349
api.go
@ -2,33 +2,34 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/knz/strtime"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"gopkg.in/ini.v1"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/knz/strtime"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func (ctx *appContext) loadStrftime() {
|
||||
ctx.datePattern = ctx.config.Section("email").Key("date_format").String()
|
||||
ctx.timePattern = `%H:%M`
|
||||
if val, _ := ctx.config.Section("email").Key("use_24h").Bool(); !val {
|
||||
ctx.timePattern = `%I:%M %p`
|
||||
func (app *appContext) loadStrftime() {
|
||||
app.datePattern = app.config.Section("email").Key("date_format").String()
|
||||
app.timePattern = `%H:%M`
|
||||
if val, _ := app.config.Section("email").Key("use_24h").Bool(); !val {
|
||||
app.timePattern = `%I:%M %p`
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *appContext) prettyTime(dt time.Time) (date, time string) {
|
||||
date, _ = strtime.Strftime(dt, ctx.datePattern)
|
||||
time, _ = strtime.Strftime(dt, ctx.timePattern)
|
||||
func (app *appContext) prettyTime(dt time.Time) (date, time string) {
|
||||
date, _ = strtime.Strftime(dt, app.datePattern)
|
||||
time, _ = strtime.Strftime(dt, app.timePattern)
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *appContext) formatDatetime(dt time.Time) string {
|
||||
d, t := ctx.prettyTime(dt)
|
||||
func (app *appContext) formatDatetime(dt time.Time) string {
|
||||
d, t := app.prettyTime(dt)
|
||||
return d + " " + t
|
||||
}
|
||||
|
||||
@ -79,60 +80,60 @@ func timeDiff(a, b time.Time) (year, month, day, hour, min, sec int) {
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *appContext) checkInvites() {
|
||||
func (app *appContext) checkInvites() {
|
||||
current_time := time.Now()
|
||||
ctx.storage.loadInvites()
|
||||
app.storage.loadInvites()
|
||||
changed := false
|
||||
for code, data := range ctx.storage.invites {
|
||||
for code, data := range app.storage.invites {
|
||||
expiry := data.ValidTill
|
||||
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
|
||||
if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
ctx.debug.Printf("%s: Expiry notification", code)
|
||||
if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", code)
|
||||
for address, settings := range notify {
|
||||
if settings["notify-expiry"] {
|
||||
go func() {
|
||||
if ctx.email.constructExpiry(code, data, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
} else if ctx.email.send(address, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to send expiry notification", code)
|
||||
if app.email.constructExpiry(code, data, app) != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
} else if app.email.send(address, app) != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification", code)
|
||||
} else {
|
||||
ctx.info.Printf("Sent expiry notification to %s", address)
|
||||
app.info.Printf("Sent expiry notification to %s", address)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
changed = true
|
||||
delete(ctx.storage.invites, code)
|
||||
delete(app.storage.invites, code)
|
||||
}
|
||||
}
|
||||
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()
|
||||
ctx.storage.loadInvites()
|
||||
app.storage.loadInvites()
|
||||
changed := false
|
||||
if inv, match := ctx.storage.invites[code]; match {
|
||||
if inv, match := app.storage.invites[code]; match {
|
||||
expiry := inv.ValidTill
|
||||
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
|
||||
if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
ctx.debug.Printf("%s: Expiry notification", code)
|
||||
if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", code)
|
||||
for address, settings := range notify {
|
||||
if settings["notify-expiry"] {
|
||||
go func() {
|
||||
if ctx.email.constructExpiry(code, inv, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
} else if ctx.email.send(address, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to send expiry notification", code)
|
||||
if app.email.constructExpiry(code, inv, app) != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
} else if app.email.send(address, app) != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification", code)
|
||||
} 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
|
||||
match = false
|
||||
delete(ctx.storage.invites, code)
|
||||
delete(app.storage.invites, code)
|
||||
} else if used {
|
||||
changed = true
|
||||
del := false
|
||||
newInv := inv
|
||||
if newInv.RemainingUses == 1 {
|
||||
del = true
|
||||
delete(ctx.storage.invites, code)
|
||||
delete(app.storage.invites, code)
|
||||
} else if newInv.RemainingUses != 0 {
|
||||
// 0 means infinite i guess?
|
||||
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 {
|
||||
ctx.storage.invites[code] = newInv
|
||||
app.storage.invites[code] = newInv
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
ctx.storage.storeInvites()
|
||||
app.storage.storeInvites()
|
||||
}
|
||||
return match
|
||||
}
|
||||
@ -174,17 +175,17 @@ type newUserReq struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) NewUser(gc *gin.Context) {
|
||||
func (app *appContext) NewUser(gc *gin.Context) {
|
||||
var req newUserReq
|
||||
gc.BindJSON(&req)
|
||||
ctx.debug.Printf("%s: New user attempt", req.Code)
|
||||
if !ctx.checkInvite(req.Code, false, "") {
|
||||
ctx.info.Printf("%s New user failed: invalid code", req.Code)
|
||||
app.debug.Printf("%s: New user attempt", req.Code)
|
||||
if !app.checkInvite(req.Code, false, "") {
|
||||
app.info.Printf("%s New user failed: invalid code", req.Code)
|
||||
gc.JSON(401, map[string]bool{"success": false})
|
||||
gc.Abort()
|
||||
return
|
||||
}
|
||||
validation := ctx.validator.validate(req.Password)
|
||||
validation := app.validator.validate(req.Password)
|
||||
valid := true
|
||||
for _, val := range validation {
|
||||
if !val {
|
||||
@ -193,38 +194,38 @@ func (ctx *appContext) NewUser(gc *gin.Context) {
|
||||
}
|
||||
if !valid {
|
||||
// 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.Abort()
|
||||
return
|
||||
}
|
||||
existingUser, _, _ := ctx.jf.userByName(req.Username, false)
|
||||
existingUser, _, _ := app.jf.userByName(req.Username, false)
|
||||
if existingUser != nil {
|
||||
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)
|
||||
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 {
|
||||
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)
|
||||
return
|
||||
}
|
||||
ctx.checkInvite(req.Code, true, req.Username)
|
||||
invite := ctx.storage.invites[req.Code]
|
||||
if ctx.config.Section("notifications").Key("enabled").MustBool(false) {
|
||||
app.checkInvite(req.Code, true, req.Username)
|
||||
invite := app.storage.invites[req.Code]
|
||||
if app.config.Section("notifications").Key("enabled").MustBool(false) {
|
||||
for address, settings := range invite.Notify {
|
||||
if settings["notify-creation"] {
|
||||
go func() {
|
||||
if ctx.email.constructCreated(req.Code, req.Username, req.Email, invite, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to construct user creation notification", req.Code)
|
||||
ctx.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} else if ctx.email.send(address, ctx) != nil {
|
||||
ctx.err.Printf("%s: Failed to send user creation notification", req.Code)
|
||||
ctx.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
if app.email.constructCreated(req.Code, req.Username, req.Email, invite, app) != nil {
|
||||
app.err.Printf("%s: Failed to construct user creation notification", req.Code)
|
||||
app.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} else if app.email.send(address, app) != nil {
|
||||
app.err.Printf("%s: Failed to send user creation notification", req.Code)
|
||||
app.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} 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 {
|
||||
id = user["Id"].(string)
|
||||
}
|
||||
if len(ctx.storage.policy) != 0 {
|
||||
status, err = ctx.jf.setPolicy(id, ctx.storage.policy)
|
||||
if len(app.storage.policy) != 0 {
|
||||
status, err = app.jf.setPolicy(id, app.storage.policy)
|
||||
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 {
|
||||
status, err = ctx.jf.setConfiguration(id, ctx.storage.configuration)
|
||||
if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 {
|
||||
status, err = app.jf.setConfiguration(id, app.storage.configuration)
|
||||
if (status == 200 || status == 204) && err == nil {
|
||||
status, err = ctx.jf.setDisplayPreferences(id, ctx.storage.displayprefs)
|
||||
status, err = app.jf.setDisplayPreferences(id, app.storage.displayprefs)
|
||||
} 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) {
|
||||
ctx.storage.emails[id] = req.Email
|
||||
ctx.storage.storeEmails()
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
app.storage.emails[id] = req.Email
|
||||
app.storage.storeEmails()
|
||||
}
|
||||
gc.JSON(200, validation)
|
||||
}
|
||||
@ -265,10 +266,10 @@ type generateInviteReq struct {
|
||||
RemainingUses int `json:"remaining-uses"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) GenerateInvite(gc *gin.Context) {
|
||||
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
var req generateInviteReq
|
||||
ctx.debug.Println("Generating new invite")
|
||||
ctx.storage.loadInvites()
|
||||
app.debug.Println("Generating new invite")
|
||||
app.storage.loadInvites()
|
||||
gc.BindJSON(&req)
|
||||
current_time := time.Now()
|
||||
valid_till := current_time.AddDate(0, 0, req.Days)
|
||||
@ -286,40 +287,40 @@ func (ctx *appContext) GenerateInvite(gc *gin.Context) {
|
||||
invite.RemainingUses = 1
|
||||
}
|
||||
invite.ValidTill = valid_till
|
||||
if req.Email != "" && ctx.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
ctx.debug.Printf("%s: Sending invite email", invite_code)
|
||||
if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
app.debug.Printf("%s: Sending invite email", invite_code)
|
||||
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)
|
||||
ctx.err.Printf("%s: Failed to construct invite email", invite_code)
|
||||
ctx.debug.Printf("%s: Error: %s", invite_code, err)
|
||||
} else if err := ctx.email.send(req.Email, ctx); err != nil {
|
||||
app.err.Printf("%s: Failed to construct invite email", invite_code)
|
||||
app.debug.Printf("%s: Error: %s", invite_code, err)
|
||||
} else if err := app.email.send(req.Email, app); err != nil {
|
||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
||||
ctx.err.Printf("%s: %s", invite_code, invite.Email)
|
||||
ctx.debug.Printf("%s: Error: %s", invite_code, err)
|
||||
app.err.Printf("%s: %s", invite_code, invite.Email)
|
||||
app.debug.Printf("%s: Error: %s", invite_code, err)
|
||||
} 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
|
||||
ctx.storage.storeInvites()
|
||||
app.storage.invites[invite_code] = invite
|
||||
app.storage.storeInvites()
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (ctx *appContext) GetInvites(gc *gin.Context) {
|
||||
ctx.debug.Println("Invites requested")
|
||||
func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
app.debug.Println("Invites requested")
|
||||
current_time := time.Now()
|
||||
ctx.storage.loadInvites()
|
||||
ctx.checkInvites()
|
||||
app.storage.loadInvites()
|
||||
app.checkInvites()
|
||||
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)
|
||||
invite := make(map[string]interface{})
|
||||
invite["code"] = code
|
||||
invite["days"] = days
|
||||
invite["hours"] = hours
|
||||
invite["minutes"] = minutes
|
||||
invite["created"] = ctx.formatDatetime(inv.Created)
|
||||
invite["created"] = app.formatDatetime(inv.Created)
|
||||
if len(inv.UsedBy) != 0 {
|
||||
invite["used-by"] = inv.UsedBy
|
||||
}
|
||||
@ -335,11 +336,11 @@ func (ctx *appContext) GetInvites(gc *gin.Context) {
|
||||
}
|
||||
if len(inv.Notify) != 0 {
|
||||
var address string
|
||||
if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
ctx.storage.loadEmails()
|
||||
address = ctx.storage.emails[gc.GetString("jfId")].(string)
|
||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
app.storage.loadEmails()
|
||||
address = app.storage.emails[gc.GetString("jfId")].(string)
|
||||
} else {
|
||||
address = ctx.config.Section("ui").Key("email").String()
|
||||
address = app.config.Section("ui").Key("email").String()
|
||||
}
|
||||
if _, ok := inv.Notify[address]; ok {
|
||||
for _, notify_type := range []string{"notify-expiry", "notify-creation"} {
|
||||
@ -362,34 +363,34 @@ type notifySetting struct {
|
||||
NotifyCreation bool `json:"notify-creation"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) SetNotify(gc *gin.Context) {
|
||||
func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
var req map[string]notifySetting
|
||||
gc.BindJSON(&req)
|
||||
changed := false
|
||||
for code, settings := range req {
|
||||
ctx.debug.Printf("%s: Notification settings change requested", code)
|
||||
ctx.storage.loadInvites()
|
||||
ctx.storage.loadEmails()
|
||||
invite, ok := ctx.storage.invites[code]
|
||||
app.debug.Printf("%s: Notification settings change requested", code)
|
||||
app.storage.loadInvites()
|
||||
app.storage.loadEmails()
|
||||
invite, ok := app.storage.invites[code]
|
||||
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.Abort()
|
||||
return
|
||||
}
|
||||
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
|
||||
address, ok = ctx.storage.emails[gc.GetString("jfId")].(string)
|
||||
address, ok = app.storage.emails[gc.GetString("jfId")].(string)
|
||||
if !ok {
|
||||
ctx.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.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
|
||||
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||
gc.JSON(500, map[string]string{"error": "Missing user email"})
|
||||
gc.Abort()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
address = ctx.config.Section("ui").Key("email").String()
|
||||
address = app.config.Section("ui").Key("email").String()
|
||||
}
|
||||
if invite.Notify == nil {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
if 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
|
||||
}
|
||||
if changed {
|
||||
ctx.storage.invites[code] = invite
|
||||
app.storage.invites[code] = invite
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
ctx.storage.storeInvites()
|
||||
app.storage.storeInvites()
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,20 +423,20 @@ type deleteReq struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) DeleteInvite(gc *gin.Context) {
|
||||
func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
var req deleteReq
|
||||
gc.BindJSON(&req)
|
||||
ctx.debug.Printf("%s: Deletion requested", req.Code)
|
||||
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||
var ok bool
|
||||
_, ok = ctx.storage.invites[req.Code]
|
||||
_, ok = app.storage.invites[req.Code]
|
||||
if ok {
|
||||
delete(ctx.storage.invites, req.Code)
|
||||
ctx.storage.storeInvites()
|
||||
ctx.info.Printf("%s: Invite deleted", req.Code)
|
||||
delete(app.storage.invites, req.Code)
|
||||
app.storage.storeInvites()
|
||||
app.info.Printf("%s: Invite deleted", req.Code)
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
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)
|
||||
}
|
||||
|
||||
@ -448,21 +449,21 @@ type respUser struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) GetUsers(gc *gin.Context) {
|
||||
ctx.debug.Println("Users requested")
|
||||
func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
app.debug.Println("Users requested")
|
||||
var resp userResp
|
||||
resp.UserList = []respUser{}
|
||||
users, status, err := ctx.jf.getUsers(false)
|
||||
users, status, err := app.jf.getUsers(false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
for _, jfUser := range users {
|
||||
var user respUser
|
||||
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)
|
||||
}
|
||||
resp.UserList = append(resp.UserList, user)
|
||||
@ -470,24 +471,24 @@ func (ctx *appContext) GetUsers(gc *gin.Context) {
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
func (ctx *appContext) ModifyEmails(gc *gin.Context) {
|
||||
func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
var req map[string]string
|
||||
gc.BindJSON(&req)
|
||||
ctx.debug.Println("Email modification requested")
|
||||
users, status, err := ctx.jf.getUsers(false)
|
||||
app.debug.Println("Email modification requested")
|
||||
users, status, err := app.jf.getUsers(false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
for _, jfUser := range users {
|
||||
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()
|
||||
ctx.info.Println("Email list modified")
|
||||
app.storage.storeEmails()
|
||||
app.info.Println("Email list modified")
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
@ -496,46 +497,46 @@ type defaultsReq struct {
|
||||
Homescreen bool `json:"homescreen"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) SetDefaults(gc *gin.Context) {
|
||||
func (app *appContext) SetDefaults(gc *gin.Context) {
|
||||
var req defaultsReq
|
||||
gc.BindJSON(&req)
|
||||
ctx.info.Printf("Getting user defaults from \"%s\"", req.Username)
|
||||
user, status, err := ctx.jf.userByName(req.Username, false)
|
||||
app.info.Printf("Getting user defaults from \"%s\"", req.Username)
|
||||
user, status, err := app.jf.userByName(req.Username, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
ctx.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
userId := user["Id"].(string)
|
||||
policy := user["Policy"].(map[string]interface{})
|
||||
ctx.storage.policy = policy
|
||||
ctx.storage.storePolicy()
|
||||
ctx.debug.Println("User policy template stored")
|
||||
app.storage.policy = policy
|
||||
app.storage.storePolicy()
|
||||
app.debug.Println("User policy template stored")
|
||||
if req.Homescreen {
|
||||
configuration := user["Configuration"].(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 {
|
||||
ctx.err.Printf("Failed to get DisplayPrefs: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get displayprefs", gc)
|
||||
return
|
||||
}
|
||||
ctx.storage.configuration = configuration
|
||||
ctx.storage.displayprefs = displayprefs
|
||||
ctx.storage.storeConfiguration()
|
||||
ctx.debug.Println("Configuration template stored")
|
||||
ctx.storage.storeDisplayprefs()
|
||||
ctx.debug.Println("DisplayPrefs template stored")
|
||||
app.storage.configuration = configuration
|
||||
app.storage.displayprefs = displayprefs
|
||||
app.storage.storeConfiguration()
|
||||
app.debug.Println("Configuration template stored")
|
||||
app.storage.storeDisplayprefs()
|
||||
app.debug.Println("DisplayPrefs template stored")
|
||||
}
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (ctx *appContext) GetConfig(gc *gin.Context) {
|
||||
ctx.info.Println("Config requested")
|
||||
func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
app.info.Println("Config requested")
|
||||
resp := map[string]interface{}{}
|
||||
for section, settings := range ctx.configBase {
|
||||
for section, settings := range app.configBase {
|
||||
if section == "order" {
|
||||
resp[section] = settings.([]interface{})
|
||||
} else {
|
||||
@ -547,7 +548,7 @@ func (ctx *appContext) GetConfig(gc *gin.Context) {
|
||||
resp[section].(map[string]interface{})[key] = values.(map[string]interface{})
|
||||
if key != "meta" {
|
||||
dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string)
|
||||
configKey := ctx.config.Section(section).Key(key)
|
||||
configKey := app.config.Section(section).Key(key)
|
||||
if dataType == "number" {
|
||||
if val, err := configKey.Int(); err == nil {
|
||||
resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val
|
||||
@ -565,11 +566,11 @@ func (ctx *appContext) GetConfig(gc *gin.Context) {
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
func (ctx *appContext) ModifyConfig(gc *gin.Context) {
|
||||
ctx.info.Println("Config modification requested")
|
||||
func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
app.info.Println("Config modification requested")
|
||||
var req map[string]interface{}
|
||||
gc.BindJSON(&req)
|
||||
tempConfig, _ := ini.Load(ctx.config_path)
|
||||
tempConfig, _ := ini.Load(app.config_path)
|
||||
for section, settings := range req {
|
||||
_, err := tempConfig.GetSection(section)
|
||||
if section != "restart-program" && err == nil {
|
||||
@ -578,33 +579,33 @@ func (ctx *appContext) ModifyConfig(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
tempConfig.SaveTo(ctx.config_path)
|
||||
ctx.debug.Println("Config saved")
|
||||
tempConfig.SaveTo(app.config_path)
|
||||
app.debug.Println("Config saved")
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
if req["restart-program"].(bool) {
|
||||
ctx.info.Println("Restarting...")
|
||||
err := ctx.Restart()
|
||||
app.info.Println("Restarting...")
|
||||
err := app.Restart()
|
||||
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.
|
||||
if _, ok := req["password_validation"]; ok {
|
||||
ctx.debug.Println("Reinitializing validator")
|
||||
app.debug.Println("Reinitializing validator")
|
||||
validatorConf := ValidatorConf{
|
||||
"characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||
"uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0),
|
||||
"lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0),
|
||||
"numbers": ctx.config.Section("password_validation").Key("number").MustInt(0),
|
||||
"special characters": ctx.config.Section("password_validation").Key("special").MustInt(0),
|
||||
"characters": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||
"uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0),
|
||||
"lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0),
|
||||
"numbers": app.config.Section("password_validation").Key("number").MustInt(0),
|
||||
"special characters": app.config.Section("password_validation").Key("special").MustInt(0),
|
||||
}
|
||||
if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||
for key := range validatorConf {
|
||||
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"))
|
||||
// }
|
||||
|
||||
func (ctx *appContext) Restart() error {
|
||||
func (app *appContext) Restart() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
signal.Notify(ctx.quit, os.Interrupt)
|
||||
<-ctx.quit
|
||||
signal.Notify(app.quit, os.Interrupt)
|
||||
<-app.quit
|
||||
}
|
||||
}()
|
||||
args := os.Args
|
||||
|
53
auth.go
53
auth.go
@ -3,22 +3,23 @@ package main
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
|
||||
func (ctx *appContext) webAuth() gin.HandlerFunc {
|
||||
return ctx.authenticate
|
||||
func (app *appContext) webAuth() gin.HandlerFunc {
|
||||
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)
|
||||
if header[0] != "Basic" {
|
||||
ctx.debug.Println("Invalid authentication header")
|
||||
app.debug.Println("Invalid authentication header")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@ -26,13 +27,13 @@ func (ctx *appContext) authenticate(gc *gin.Context) {
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
token, err := jwt.Parse(creds[0], func(token *jwt.Token) (interface{}, error) {
|
||||
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 []byte(os.Getenv("JFA_SECRET")), nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.debug.Printf("Auth denied: %s", err)
|
||||
app.debug.Printf("Auth denied: %s", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@ -43,32 +44,32 @@ func (ctx *appContext) authenticate(gc *gin.Context) {
|
||||
userId = claims["id"].(string)
|
||||
jfId = claims["jfid"].(string)
|
||||
} else {
|
||||
ctx.debug.Printf("Invalid token")
|
||||
app.debug.Printf("Invalid token")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
match := false
|
||||
for _, user := range ctx.users {
|
||||
for _, user := range app.users {
|
||||
if user.UserID == userId {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
gc.Set("jfId", jfId)
|
||||
gc.Set("userId", userId)
|
||||
ctx.debug.Println("Authentication successful")
|
||||
app.debug.Println("Authentication successful")
|
||||
gc.Next()
|
||||
}
|
||||
|
||||
func (ctx *appContext) GetToken(gc *gin.Context) {
|
||||
ctx.info.Println("Token requested (login attempt)")
|
||||
func (app *appContext) GetToken(gc *gin.Context) {
|
||||
app.info.Println("Token requested (login attempt)")
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
if header[0] != "Basic" {
|
||||
ctx.debug.Println("Invalid authentication header")
|
||||
app.debug.Println("Invalid authentication header")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@ -76,7 +77,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
match := false
|
||||
var userId string
|
||||
for _, user := range ctx.users {
|
||||
for _, user := range app.users {
|
||||
if user.Username == creds[0] && user.Password == creds[1] {
|
||||
match = true
|
||||
userId = user.UserID
|
||||
@ -84,29 +85,29 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
|
||||
}
|
||||
jfId := ""
|
||||
if !match {
|
||||
if !ctx.jellyfinLogin {
|
||||
ctx.info.Println("Auth failed: Invalid username and/or password")
|
||||
if !app.jellyfinLogin {
|
||||
app.info.Println("Auth failed: Invalid username and/or password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
var status int
|
||||
var err error
|
||||
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)
|
||||
if status != 200 || err != nil {
|
||||
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)
|
||||
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)
|
||||
return
|
||||
} 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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -114,8 +115,8 @@ func (ctx *appContext) GetToken(gc *gin.Context) {
|
||||
newuser.UserID = shortuuid.New()
|
||||
userId = newuser.UserID
|
||||
// uuid, nothing else identifiable!
|
||||
ctx.debug.Printf("Token generated for user \"%s\"", creds[0])
|
||||
ctx.users = append(ctx.users, newuser)
|
||||
app.debug.Printf("Token generated for user \"%s\"", creds[0])
|
||||
app.users = append(app.users, newuser)
|
||||
}
|
||||
}
|
||||
token, err := CreateToken(userId, jfId)
|
||||
|
45
config.go
45
config.go
@ -1,9 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/ini.v1"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
/*var DeCamel ini.NameMapper = func(raw string) string {
|
||||
@ -22,51 +23,51 @@ import (
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func (ctx *appContext) loadDefaults() (err error) {
|
||||
func (app *appContext) loadDefaults() (err error) {
|
||||
var cfb []byte
|
||||
cfb, err = ioutil.ReadFile(ctx.configBase_path)
|
||||
cfb, err = ioutil.ReadFile(app.configBase_path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
json.Unmarshal(cfb, ctx.defaults)
|
||||
json.Unmarshal(cfb, app.defaults)
|
||||
return
|
||||
}*/
|
||||
|
||||
func (ctx *appContext) loadConfig() error {
|
||||
func (app *appContext) loadConfig() error {
|
||||
var err error
|
||||
ctx.config, err = ini.Load(ctx.config_path)
|
||||
app.config, err = ini.Load(app.config_path)
|
||||
if err != nil {
|
||||
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" {
|
||||
// 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"} {
|
||||
// if ctx.config.Section("files").Key(key).MustString("") == "" {
|
||||
// key.SetValue(filepath.Join(ctx.data_path, (key.Name() + ".json")))
|
||||
// if app.config.Section("files").Key(key).MustString("") == "" {
|
||||
// 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")))
|
||||
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_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString(filepath.Join(app.local_path, "email.html")))
|
||||
app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString(filepath.Join(app.local_path, "email.txt")))
|
||||
|
||||
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")))
|
||||
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_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(app.local_path, "invite-email.html")))
|
||||
app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(app.local_path, "invite-email.txt")))
|
||||
|
||||
ctx.config.Section("notifications").Key("expiry_html").SetValue(ctx.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(ctx.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_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.local_path, "expired.html")))
|
||||
app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.local_path, "expired.txt")))
|
||||
|
||||
ctx.config.Section("notifications").Key("created_html").SetValue(ctx.config.Section("notifications").Key("created_html").MustString(filepath.Join(ctx.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_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.local_path, "created.html")))
|
||||
app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.local_path, "created.txt")))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
14
daemon.go
14
daemon.go
@ -9,21 +9,21 @@ type Repeater struct {
|
||||
ShutdownChannel chan string
|
||||
Interval 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{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
Interval: interval,
|
||||
period: interval,
|
||||
ctx: ctx,
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Repeater) Run() {
|
||||
rt.ctx.info.Println("Invite daemon started")
|
||||
rt.app.info.Println("Invite daemon started")
|
||||
for {
|
||||
select {
|
||||
case <-rt.ShutdownChannel:
|
||||
@ -33,9 +33,9 @@ func (rt *Repeater) Run() {
|
||||
break
|
||||
}
|
||||
started := time.Now()
|
||||
rt.ctx.storage.loadInvites()
|
||||
rt.ctx.debug.Println("Daemon: Checking invites")
|
||||
rt.ctx.checkInvites()
|
||||
rt.app.storage.loadInvites()
|
||||
rt.app.debug.Println("Daemon: Checking invites")
|
||||
rt.app.checkInvites()
|
||||
finished := time.Now()
|
||||
duration := finished.Sub(started)
|
||||
rt.period = rt.Interval - duration
|
||||
|
77
email.go
77
email.go
@ -5,13 +5,14 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
jEmail "github.com/jordan-wright/email"
|
||||
"github.com/knz/strtime"
|
||||
"github.com/mailgun/mailgun-go/v4"
|
||||
"html/template"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jEmail "github.com/jordan-wright/email"
|
||||
"github.com/knz/strtime"
|
||||
"github.com/mailgun/mailgun-go/v4"
|
||||
)
|
||||
|
||||
type Emailer struct {
|
||||
@ -49,13 +50,13 @@ func (email *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern,
|
||||
return
|
||||
}
|
||||
|
||||
func (email *Emailer) init(ctx *appContext) {
|
||||
email.fromAddr = ctx.config.Section("email").Key("address").String()
|
||||
email.fromName = ctx.config.Section("email").Key("from").String()
|
||||
email.sendMethod = ctx.config.Section("email").Key("method").String()
|
||||
func (email *Emailer) init(app *appContext) {
|
||||
email.fromAddr = app.config.Section("email").Key("address").String()
|
||||
email.fromName = app.config.Section("email").Key("from").String()
|
||||
email.sendMethod = app.config.Section("email").Key("method").String()
|
||||
if email.sendMethod == "mailgun" {
|
||||
email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], ctx.config.Section("mailgun").Key("api_key").String())
|
||||
api_url := ctx.config.Section("mailgun").Key("api_url").String()
|
||||
email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], app.config.Section("mailgun").Key("api_key").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'
|
||||
if strings.Contains(api_url, "messages") {
|
||||
api_url = api_url[0:strings.LastIndex(api_url, "/")]
|
||||
@ -63,21 +64,21 @@ func (email *Emailer) init(ctx *appContext) {
|
||||
}
|
||||
email.mg.SetAPIBase(api_url)
|
||||
} else if email.sendMethod == "smtp" {
|
||||
ctx.host = ctx.config.Section("smtp").Key("server").String()
|
||||
email.smtpAuth = smtp.PlainAuth("", email.fromAddr, ctx.config.Section("smtp").Key("password").String(), ctx.host)
|
||||
app.host = app.config.Section("smtp").Key("server").String()
|
||||
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 {
|
||||
email.content.subject = ctx.config.Section("invite_emails").Key("subject").String()
|
||||
func (email *Emailer) constructInvite(code string, invite Invite, app *appContext) error {
|
||||
email.content.subject = app.config.Section("invite_emails").Key("subject").String()
|
||||
expiry := invite.ValidTill
|
||||
d, t, expires_in := email.formatExpiry(expiry, false, ctx.datePattern, ctx.timePattern)
|
||||
message := ctx.config.Section("email").Key("message").String()
|
||||
invite_link := ctx.config.Section("invite_emails").Key("url_base").String()
|
||||
d, t, expires_in := email.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
invite_link := app.config.Section("invite_emails").Key("url_base").String()
|
||||
invite_link = fmt.Sprintf("%s/%s", invite_link, code)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -103,11 +104,11 @@ func (email *Emailer) constructInvite(code string, invite Invite, ctx *appContex
|
||||
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"
|
||||
expiry := ctx.formatDatetime(invite.ValidTill)
|
||||
expiry := app.formatDatetime(invite.ValidTill)
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -130,17 +131,17 @@ func (email *Emailer) constructExpiry(code string, invite Invite, ctx *appContex
|
||||
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"
|
||||
created := ctx.formatDatetime(invite.Created)
|
||||
created := app.formatDatetime(invite.Created)
|
||||
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"
|
||||
} else {
|
||||
tplAddress = address
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -165,12 +166,12 @@ func (email *Emailer) constructCreated(code, username, address string, invite In
|
||||
return nil
|
||||
}
|
||||
|
||||
func (email *Emailer) constructReset(pwr Pwr, ctx *appContext) error {
|
||||
email.content.subject = ctx.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin")
|
||||
d, t, expires_in := email.formatExpiry(pwr.Expiry, true, ctx.datePattern, ctx.timePattern)
|
||||
message := ctx.config.Section("email").Key("message").String()
|
||||
func (email *Emailer) constructReset(pwr Pwr, app *appContext) error {
|
||||
email.content.subject = app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin")
|
||||
d, t, expires_in := email.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -197,7 +198,7 @@ func (email *Emailer) constructReset(pwr Pwr, ctx *appContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (email *Emailer) send(address string, ctx *appContext) error {
|
||||
func (email *Emailer) send(address string, app *appContext) error {
|
||||
if email.sendMethod == "mailgun" {
|
||||
message := email.mg.NewMessage(
|
||||
fmt.Sprintf("%s <%s>", email.fromName, email.fromAddr),
|
||||
@ -205,9 +206,9 @@ func (email *Emailer) send(address string, ctx *appContext) error {
|
||||
email.content.text,
|
||||
address)
|
||||
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()
|
||||
_, _, err := email.mg.Send(mgctx, message)
|
||||
_, _, err := email.mg.Send(mgapp, message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -218,19 +219,19 @@ func (email *Emailer) send(address string, ctx *appContext) error {
|
||||
e.To = []string{address}
|
||||
e.Text = []byte(email.content.text)
|
||||
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{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: ctx.host,
|
||||
ServerName: app.host,
|
||||
}
|
||||
var err error
|
||||
if smtpType == "ssl_tls" {
|
||||
port := ctx.config.Section("smtp").Key("port").MustInt(465)
|
||||
server := fmt.Sprintf("%s:%d", ctx.host, port)
|
||||
port := app.config.Section("smtp").Key("port").MustInt(465)
|
||||
server := fmt.Sprintf("%s:%d", app.host, port)
|
||||
err = e.SendWithTLS(server, email.smtpAuth, tlsConfig)
|
||||
} else if smtpType == "starttls" {
|
||||
port := ctx.config.Section("smtp").Key("port").MustInt(587)
|
||||
server := fmt.Sprintf("%s:%d", ctx.host, port)
|
||||
port := app.config.Section("smtp").Key("port").MustInt(587)
|
||||
server := fmt.Sprintf("%s:%d", app.host, port)
|
||||
e.SendWithStartTLS(server, email.smtpAuth, tlsConfig)
|
||||
}
|
||||
return err
|
||||
|
253
main.go
253
main.go
@ -7,11 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"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/ioutil"
|
||||
"log"
|
||||
@ -20,6 +15,12 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"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!
|
||||
@ -95,190 +96,190 @@ func setGinLogger(router *gin.Engine, debugMode bool) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := new(appContext)
|
||||
app := new(appContext)
|
||||
userConfigDir, _ := os.UserConfigDir()
|
||||
ctx.data_path = filepath.Join(userConfigDir, "jfa-go")
|
||||
ctx.config_path = filepath.Join(ctx.data_path, "config.ini")
|
||||
app.data_path = filepath.Join(userConfigDir, "jfa-go")
|
||||
app.config_path = filepath.Join(app.data_path, "config.ini")
|
||||
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)
|
||||
ctx.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile)
|
||||
app.info = log.New(os.Stdout, "[INFO] ", log.Ltime)
|
||||
app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile)
|
||||
|
||||
dataPath := flag.String("data", ctx.data_path, "alternate path to data directory.")
|
||||
configPath := flag.String("config", ctx.config_path, "alternate path to config file.")
|
||||
dataPath := flag.String("data", app.data_path, "alternate path to data directory.")
|
||||
configPath := flag.String("config", app.config_path, "alternate path to config file.")
|
||||
host := flag.String("host", "", "alternate address to host web ui on.")
|
||||
port := flag.Int("port", 0, "alternate port to host web ui on.")
|
||||
|
||||
flag.Parse()
|
||||
if ctx.config_path == *configPath && ctx.data_path != *dataPath {
|
||||
ctx.config_path = filepath.Join(*dataPath, "config.ini")
|
||||
if app.config_path == *configPath && app.data_path != *dataPath {
|
||||
app.config_path = filepath.Join(*dataPath, "config.ini")
|
||||
} else {
|
||||
ctx.config_path = *configPath
|
||||
ctx.data_path = *dataPath
|
||||
app.config_path = *configPath
|
||||
app.data_path = *dataPath
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
ctx.config_path = v
|
||||
app.config_path = 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_DATAPATH", ctx.data_path)
|
||||
os.Setenv("JFA_CONFIGPATH", app.config_path)
|
||||
os.Setenv("JFA_DATAPATH", app.data_path)
|
||||
|
||||
var firstRun bool
|
||||
if _, err := os.Stat(ctx.data_path); os.IsNotExist(err) {
|
||||
os.Mkdir(ctx.data_path, 0700)
|
||||
if _, err := os.Stat(app.data_path); os.IsNotExist(err) {
|
||||
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
|
||||
dConfigPath := filepath.Join(ctx.local_path, "config-default.ini")
|
||||
dConfigPath := filepath.Join(app.local_path, "config-default.ini")
|
||||
var dConfig *os.File
|
||||
dConfig, err = os.Open(dConfigPath)
|
||||
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()
|
||||
var nConfig *os.File
|
||||
nConfig, err := os.Create(ctx.config_path)
|
||||
nConfig, err := os.Create(app.config_path)
|
||||
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()
|
||||
_, err = io.Copy(nConfig, dConfig)
|
||||
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 address string
|
||||
if ctx.loadConfig() != nil {
|
||||
ctx.err.Fatalf("Failed to load config file \"%s\"", ctx.config_path)
|
||||
if app.loadConfig() != nil {
|
||||
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 {
|
||||
ctx.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile)
|
||||
app.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile)
|
||||
} else {
|
||||
ctx.debug = log.New(ioutil.Discard, "", 0)
|
||||
app.debug = log.New(ioutil.Discard, "", 0)
|
||||
}
|
||||
|
||||
if !firstRun {
|
||||
ctx.host = ctx.config.Section("ui").Key("host").String()
|
||||
ctx.port = ctx.config.Section("ui").Key("port").MustInt(8056)
|
||||
app.host = app.config.Section("ui").Key("host").String()
|
||||
app.port = app.config.Section("ui").Key("port").MustInt(8056)
|
||||
|
||||
if *host != ctx.host && *host != "" {
|
||||
ctx.host = *host
|
||||
if *host != app.host && *host != "" {
|
||||
app.host = *host
|
||||
}
|
||||
if *port != ctx.port && *port > 0 {
|
||||
ctx.port = *port
|
||||
if *port != app.port && *port > 0 {
|
||||
app.port = *port
|
||||
}
|
||||
|
||||
if h := os.Getenv("JFA_HOST"); h != "" {
|
||||
ctx.host = h
|
||||
app.host = h
|
||||
if p := os.Getenv("JFA_PORT"); p != "" {
|
||||
var port int
|
||||
_, err := fmt.Sscan(p, &port)
|
||||
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) {
|
||||
ctx.cssFile = "bs5-jf.css"
|
||||
ctx.bsVersion = 5
|
||||
if app.config.Section("ui").Key("bs5").MustBool(false) {
|
||||
app.cssFile = "bs5-jf.css"
|
||||
app.bsVersion = 5
|
||||
} else {
|
||||
ctx.cssFile = "bs4-jf.css"
|
||||
ctx.bsVersion = 4
|
||||
app.cssFile = "bs4-jf.css"
|
||||
app.bsVersion = 4
|
||||
}
|
||||
|
||||
ctx.debug.Println("Loading storage")
|
||||
app.debug.Println("Loading storage")
|
||||
|
||||
ctx.storage.invite_path = filepath.Join(ctx.data_path, "invites.json")
|
||||
ctx.storage.loadInvites()
|
||||
ctx.storage.emails_path = filepath.Join(ctx.data_path, "emails.json")
|
||||
ctx.storage.loadEmails()
|
||||
ctx.storage.policy_path = filepath.Join(ctx.data_path, "user_template.json")
|
||||
ctx.storage.loadPolicy()
|
||||
ctx.storage.configuration_path = filepath.Join(ctx.data_path, "user_configuration.json")
|
||||
ctx.storage.loadConfiguration()
|
||||
ctx.storage.displayprefs_path = filepath.Join(ctx.data_path, "user_displayprefs.json")
|
||||
ctx.storage.loadDisplayprefs()
|
||||
app.storage.invite_path = filepath.Join(app.data_path, "invites.json")
|
||||
app.storage.loadInvites()
|
||||
app.storage.emails_path = filepath.Join(app.data_path, "emails.json")
|
||||
app.storage.loadEmails()
|
||||
app.storage.policy_path = filepath.Join(app.data_path, "user_template.json")
|
||||
app.storage.loadPolicy()
|
||||
app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
|
||||
app.storage.loadConfiguration()
|
||||
app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
|
||||
app.storage.loadDisplayprefs()
|
||||
|
||||
ctx.configBase_path = filepath.Join(ctx.local_path, "config-base.json")
|
||||
config_base, _ := ioutil.ReadFile(ctx.configBase_path)
|
||||
json.Unmarshal(config_base, &ctx.configBase)
|
||||
app.configBase_path = filepath.Join(app.local_path, "config-base.json")
|
||||
config_base, _ := ioutil.ReadFile(app.configBase_path)
|
||||
json.Unmarshal(config_base, &app.configBase)
|
||||
|
||||
themes := map[string]string{
|
||||
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", ctx.bsVersion),
|
||||
"Bootstrap (Light)": fmt.Sprintf("bs%d.css", ctx.bsVersion),
|
||||
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", app.bsVersion),
|
||||
"Bootstrap (Light)": fmt.Sprintf("bs%d.css", app.bsVersion),
|
||||
"Custom CSS": "",
|
||||
}
|
||||
if val, ok := themes[ctx.config.Section("ui").Key("theme").String()]; ok {
|
||||
ctx.cssFile = val
|
||||
if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok {
|
||||
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)
|
||||
if err != nil {
|
||||
ctx.err.Fatal(err)
|
||||
app.err.Fatal(err)
|
||||
}
|
||||
os.Setenv("JFA_SECRET", secret)
|
||||
ctx.jellyfinLogin = true
|
||||
if val, _ := ctx.config.Section("ui").Key("jellyfin_login").Bool(); !val {
|
||||
ctx.jellyfinLogin = false
|
||||
app.jellyfinLogin = true
|
||||
if val, _ := app.config.Section("ui").Key("jellyfin_login").Bool(); !val {
|
||||
app.jellyfinLogin = false
|
||||
user := User{}
|
||||
user.UserID = shortuuid.New()
|
||||
user.Username = ctx.config.Section("ui").Key("username").String()
|
||||
user.Password = ctx.config.Section("ui").Key("password").String()
|
||||
ctx.users = append(ctx.users, user)
|
||||
user.Username = app.config.Section("ui").Key("username").String()
|
||||
user.Password = app.config.Section("ui").Key("password").String()
|
||||
app.users = append(app.users, user)
|
||||
} else {
|
||||
ctx.debug.Println("Using Jellyfin for authentication")
|
||||
app.debug.Println("Using Jellyfin for authentication")
|
||||
}
|
||||
|
||||
server := ctx.config.Section("jellyfin").Key("server").String()
|
||||
ctx.jf.init(server, "jfa-go", ctx.version, "hrfee-arch", "hrfee-arch")
|
||||
server := app.config.Section("jellyfin").Key("server").String()
|
||||
app.jf.init(server, "jfa-go", app.version, "hrfee-arch", "hrfee-arch")
|
||||
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 {
|
||||
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)
|
||||
ctx.authJf.init(server, "jfa-go", ctx.version, "auth", "auth")
|
||||
app.info.Printf("Authenticated with %s", server)
|
||||
app.authJf.init(server, "jfa-go", app.version, "auth", "auth")
|
||||
|
||||
ctx.loadStrftime()
|
||||
app.loadStrftime()
|
||||
|
||||
validatorConf := ValidatorConf{
|
||||
"characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||
"uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0),
|
||||
"lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0),
|
||||
"numbers": ctx.config.Section("password_validation").Key("number").MustInt(0),
|
||||
"special characters": ctx.config.Section("password_validation").Key("special").MustInt(0),
|
||||
"characters": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||
"uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0),
|
||||
"lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0),
|
||||
"numbers": app.config.Section("password_validation").Key("number").MustInt(0),
|
||||
"special characters": app.config.Section("password_validation").Key("special").MustInt(0),
|
||||
}
|
||||
if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||
for key := range validatorConf {
|
||||
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()
|
||||
|
||||
if ctx.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
go ctx.StartPWR()
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
go app.StartPWR()
|
||||
}
|
||||
} else {
|
||||
debugMode = false
|
||||
@ -286,43 +287,43 @@ func main() {
|
||||
address = "0.0.0.0:8056"
|
||||
}
|
||||
|
||||
ctx.info.Println("Loading routes")
|
||||
app.info.Println("Loading routes")
|
||||
router := gin.New()
|
||||
|
||||
setGinLogger(router, debugMode)
|
||||
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(static.Serve("/", static.LocalFile(filepath.Join(ctx.local_path, "static"), false)))
|
||||
router.LoadHTMLGlob(filepath.Join(ctx.local_path, "templates", "*"))
|
||||
router.NoRoute(ctx.NoRouteHandler)
|
||||
router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
|
||||
router.LoadHTMLGlob(filepath.Join(app.local_path, "templates", "*"))
|
||||
router.NoRoute(app.NoRouteHandler)
|
||||
if debugMode {
|
||||
ctx.debug.Println("Loading pprof")
|
||||
app.debug.Println("Loading pprof")
|
||||
pprof.Register(router)
|
||||
}
|
||||
if !firstRun {
|
||||
router.GET("/", ctx.AdminPage)
|
||||
router.GET("/getToken", ctx.GetToken)
|
||||
router.POST("/newUser", ctx.NewUser)
|
||||
router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(ctx.local_path, "static"), false)))
|
||||
router.GET("/invite/:invCode", ctx.InviteProxy)
|
||||
api := router.Group("/", ctx.webAuth())
|
||||
api.POST("/generateInvite", ctx.GenerateInvite)
|
||||
api.GET("/getInvites", ctx.GetInvites)
|
||||
api.POST("/setNotify", ctx.SetNotify)
|
||||
api.POST("/deleteInvite", ctx.DeleteInvite)
|
||||
api.GET("/getUsers", ctx.GetUsers)
|
||||
api.POST("/modifyUsers", ctx.ModifyEmails)
|
||||
api.POST("/setDefaults", ctx.SetDefaults)
|
||||
api.GET("/getConfig", ctx.GetConfig)
|
||||
api.POST("/modifyConfig", ctx.ModifyConfig)
|
||||
ctx.info.Printf("Starting router @ %s", address)
|
||||
router.GET("/", app.AdminPage)
|
||||
router.GET("/getToken", app.GetToken)
|
||||
router.POST("/newUser", app.NewUser)
|
||||
router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
|
||||
router.GET("/invite/:invCode", app.InviteProxy)
|
||||
api := router.Group("/", app.webAuth())
|
||||
api.POST("/generateInvite", app.GenerateInvite)
|
||||
api.GET("/getInvites", app.GetInvites)
|
||||
api.POST("/setNotify", app.SetNotify)
|
||||
api.POST("/deleteInvite", app.DeleteInvite)
|
||||
api.GET("/getUsers", app.GetUsers)
|
||||
api.POST("/modifyUsers", app.ModifyEmails)
|
||||
api.POST("/setDefaults", app.SetDefaults)
|
||||
api.GET("/getConfig", app.GetConfig)
|
||||
api.POST("/modifyConfig", app.ModifyConfig)
|
||||
app.info.Printf("Starting router @ %s", address)
|
||||
} else {
|
||||
router.GET("/", func(gc *gin.Context) {
|
||||
gc.HTML(200, "setup.html", gin.H{})
|
||||
})
|
||||
router.POST("/testJF", ctx.TestJF)
|
||||
router.POST("/modifyConfig", ctx.ModifyConfig)
|
||||
ctx.info.Printf("Loading setup @ %s", address)
|
||||
router.POST("/testJF", app.TestJF)
|
||||
router.POST("/modifyConfig", app.ModifyConfig)
|
||||
app.info.Printf("Loading setup @ %s", address)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
@ -331,17 +332,17 @@ func main() {
|
||||
}
|
||||
go func() {
|
||||
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)
|
||||
signal.Notify(ctx.quit, os.Interrupt)
|
||||
<-ctx.quit
|
||||
ctx.info.Println("Shutting down...")
|
||||
app.quit = make(chan os.Signal)
|
||||
signal.Notify(app.quit, os.Interrupt)
|
||||
<-app.quit
|
||||
app.info.Println("Shutting down...")
|
||||
|
||||
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(cntx); err != nil {
|
||||
ctx.err.Fatalf("Server shutdown error: %s", err)
|
||||
app.err.Fatalf("Server shutdown error: %s", err)
|
||||
}
|
||||
}
|
||||
|
47
pwreset.go
47
pwreset.go
@ -2,33 +2,34 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
func (ctx *appContext) StartPWR() {
|
||||
ctx.info.Println("Starting password reset daemon")
|
||||
path := ctx.config.Section("password_resets").Key("watch_directory").String()
|
||||
func (app *appContext) StartPWR() {
|
||||
app.info.Println("Starting password reset daemon")
|
||||
path := app.config.Section("password_resets").Key("watch_directory").String()
|
||||
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
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
ctx.err.Printf("Couldn't initialise password reset daemon")
|
||||
app.err.Printf("Couldn't initialise password reset daemon")
|
||||
return
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
done := make(chan bool)
|
||||
go pwrMonitor(ctx, watcher)
|
||||
go pwrMonitor(app, watcher)
|
||||
err = watcher.Add(path)
|
||||
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
|
||||
}
|
||||
@ -39,7 +40,7 @@ type Pwr struct {
|
||||
Expiry time.Time `json:"ExpirationDate"`
|
||||
}
|
||||
|
||||
func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) {
|
||||
func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
@ -56,29 +57,29 @@ func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) {
|
||||
if len(pwr.Pin) == 0 || err != nil {
|
||||
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) {
|
||||
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 {
|
||||
ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
ctx.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
return
|
||||
}
|
||||
ctx.storage.loadEmails()
|
||||
address, ok := ctx.storage.emails[user["Id"].(string)].(string)
|
||||
app.storage.loadEmails()
|
||||
address, ok := app.storage.emails[user["Id"].(string)].(string)
|
||||
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
|
||||
}
|
||||
if ctx.email.constructReset(pwr, ctx) != nil {
|
||||
ctx.err.Printf("Failed to construct password reset email for %s", pwr.Username)
|
||||
} else if ctx.email.send(address, ctx) != nil {
|
||||
ctx.err.Printf("Failed to send password reset email to \"%s\"", address)
|
||||
if app.email.constructReset(pwr, app) != nil {
|
||||
app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
|
||||
} else if app.email.send(address, app) != nil {
|
||||
app.err.Printf("Failed to send password reset email to \"%s\"", address)
|
||||
} else {
|
||||
ctx.info.Printf("Sent password reset email to \"%s\"", address)
|
||||
app.info.Printf("Sent password reset email to \"%s\"", address)
|
||||
}
|
||||
} 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 {
|
||||
return
|
||||
}
|
||||
ctx.err.Printf("Password reset daemon: %s", err)
|
||||
app.err.Printf("Password reset daemon: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
setup.go
6
setup.go
@ -10,14 +10,14 @@ type testReq struct {
|
||||
Password string `json:"jfPassword"`
|
||||
}
|
||||
|
||||
func (ctx *appContext) TestJF(gc *gin.Context) {
|
||||
func (app *appContext) TestJF(gc *gin.Context) {
|
||||
var req testReq
|
||||
gc.BindJSON(&req)
|
||||
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)
|
||||
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})
|
||||
return
|
||||
}
|
||||
|
53
views.go
53
views.go
@ -1,54 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (ctx *appContext) AdminPage(gc *gin.Context) {
|
||||
bs5 := ctx.config.Section("ui").Key("bs5").MustBool(false)
|
||||
emailEnabled, _ := ctx.config.Section("invite_emails").Key("enabled").Bool()
|
||||
notificationsEnabled, _ := ctx.config.Section("notifications").Key("enabled").Bool()
|
||||
func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
bs5 := app.config.Section("ui").Key("bs5").MustBool(false)
|
||||
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
||||
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
||||
gc.HTML(http.StatusOK, "admin.html", gin.H{
|
||||
"bs5": bs5,
|
||||
"cssFile": ctx.cssFile,
|
||||
"cssFile": app.cssFile,
|
||||
"contactMessage": "",
|
||||
"email_enabled": emailEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
func (ctx *appContext) InviteProxy(gc *gin.Context) {
|
||||
func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
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. */
|
||||
// if ctx.checkInvite(code, false, "") {
|
||||
if _, ok := ctx.storage.invites[code]; ok {
|
||||
email := ctx.storage.invites[code].Email
|
||||
// if app.checkInvite(code, false, "") {
|
||||
if _, ok := app.storage.invites[code]; ok {
|
||||
email := app.storage.invites[code].Email
|
||||
gc.HTML(http.StatusOK, "form.html", gin.H{
|
||||
"bs5": ctx.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": ctx.cssFile,
|
||||
"contactMessage": ctx.config.Section("ui").Key("contac_message").String(),
|
||||
"helpMessage": ctx.config.Section("ui").Key("help_message").String(),
|
||||
"successMessage": ctx.config.Section("ui").Key("success_message").String(),
|
||||
"jfLink": ctx.config.Section("jellyfin").Key("public_server").String(),
|
||||
"validate": ctx.config.Section("password_validation").Key("enabled").MustBool(false),
|
||||
"requirements": ctx.validator.getCriteria(),
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": app.cssFile,
|
||||
"contactMessage": app.config.Section("ui").Key("contac_message").String(),
|
||||
"helpMessage": app.config.Section("ui").Key("help_message").String(),
|
||||
"successMessage": app.config.Section("ui").Key("success_message").String(),
|
||||
"jfLink": app.config.Section("jellyfin").Key("public_server").String(),
|
||||
"validate": app.config.Section("password_validation").Key("enabled").MustBool(false),
|
||||
"requirements": app.validator.getCriteria(),
|
||||
"email": email,
|
||||
"username": !ctx.config.Section("email").Key("no_username").MustBool(false),
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
})
|
||||
} else {
|
||||
gc.HTML(404, "invalidCode.html", gin.H{
|
||||
"bs5": ctx.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": ctx.cssFile,
|
||||
"contactMessage": ctx.config.Section("ui").Key("contac_message").String(),
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": app.cssFile,
|
||||
"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{
|
||||
"bs5": ctx.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": ctx.cssFile,
|
||||
"contactMessage": ctx.config.Section("ui").Key("contact_message").String(),
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": app.cssFile,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user