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 (
|
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
|
||||||
|
53
auth.go
53
auth.go
@ -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)
|
||||||
|
45
config.go
45
config.go
@ -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
|
||||||
}
|
}
|
||||||
|
14
daemon.go
14
daemon.go
@ -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
|
||||||
|
77
email.go
77
email.go
@ -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
|
||||||
|
253
main.go
253
main.go
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
pwreset.go
47
pwreset.go
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
setup.go
6
setup.go
@ -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
|
||||||
}
|
}
|
||||||
|
53
views.go
53
views.go
@ -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…
Reference in New Issue
Block a user