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

fix race condition; rename route functions; fix swagger params

fix race condition when notifying of invite expiry, rename custom email
related functions as to reduce confusion, and add proper path params for
some swagger routes. Also moved some stuff around in api.go.
This commit is contained in:
Harvey Tindall 2021-05-02 20:41:08 +01:00
parent 87ef71b415
commit 5d8f139356
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
2 changed files with 211 additions and 192 deletions

385
api.go
View File

@ -160,20 +160,25 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
notify := inv.Notify notify := inv.Notify
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
app.debug.Printf("%s: Expiry notification", code) app.debug.Printf("%s: Expiry notification", code)
var wait sync.WaitGroup
for address, settings := range notify { for address, settings := range notify {
if settings["notify-expiry"] { if !settings["notify-expiry"] {
go func() { continue
}
wait.Add(1)
go func(addr string) {
defer wait.Done()
msg, err := app.email.constructExpiry(code, inv, app, false) msg, err := app.email.constructExpiry(code, inv, app, false)
if err != nil { if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err) app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
} else if err := app.email.send(msg, address); err != nil { } else if err := app.email.send(msg, addr); err != nil {
app.err.Printf("%s: Failed to send expiry notification: %v", code, err) app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
} else { } else {
app.info.Printf("Sent expiry notification to %s", address) app.info.Printf("Sent expiry notification to %s", addr)
}
}()
} }
}(address)
} }
wait.Wait()
} }
changed = true changed = true
match = false match = false
@ -313,6 +318,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
type errorFunc func(gc *gin.Context) type errorFunc func(gc *gin.Context)
// Used on the form & when a users email has been confirmed.
func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, success bool) { func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, success bool) {
existingUser, _, _ := app.jf.UserByName(req.Username, false) existingUser, _, _ := app.jf.UserByName(req.Username, false)
if existingUser.Name != "" { if existingUser.Name != "" {
@ -460,38 +466,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
return return
} }
// @Summary Extend time before the user(s) expiry.
// @Produce json
// @Param extendExpiryDTO body extendExpiryDTO true "Extend expiry object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/extend [post]
// @tags Users
func (app *appContext) ExtendExpiry(gc *gin.Context) {
var req extendExpiryDTO
gc.BindJSON(&req)
app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users))
if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 {
respondBool(400, false, gc)
return
}
app.storage.usersLock.Lock()
defer app.storage.usersLock.Unlock()
for _, id := range req.Users {
if expiry, ok := app.storage.users[id]; ok {
app.storage.users[id] = expiry.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)
app.debug.Printf("Expiry extended for \"%s\"", id)
}
}
if err := app.storage.storeUsers(); err != nil {
app.err.Printf("Failed to store user duration: %v", err)
respondBool(500, false, gc)
return
}
respondBool(204, true, gc)
}
// @Summary Creates a new Jellyfin user via invite code // @Summary Creates a new Jellyfin user via invite code
// @Produce json // @Produce json
// @Param newUserDTO body newUserDTO true "New user request object" // @Param newUserDTO body newUserDTO true "New user request object"
@ -535,44 +509,6 @@ func (app *appContext) NewUser(gc *gin.Context) {
gc.JSON(code, validation) gc.JSON(code, validation)
} }
// @Summary Send an announcement via email to a given list of users.
// @Produce json
// @Param announcementDTO body announcementDTO true "Announcement request object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/announce [post]
// @Security Bearer
// @tags Users
func (app *appContext) Announce(gc *gin.Context) {
var req announcementDTO
gc.BindJSON(&req)
if !emailEnabled {
respondBool(400, false, gc)
return
}
addresses := []string{}
for _, userID := range req.Users {
addr, ok := app.storage.emails[userID]
if !ok || addr == "" {
continue
}
addresses = append(addresses, addr.(string))
}
msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
if err != nil {
app.err.Printf("Failed to construct announcement emails: %v", err)
respondBool(500, false, gc)
return
} else if err := app.email.send(msg, addresses...); err != nil {
app.err.Printf("Failed to send announcement emails: %v", err)
respondBool(500, false, gc)
return
}
app.info.Printf("Sent announcement email to %d users", len(addresses))
respondBool(200, true, gc)
}
// @Summary Enable/Disable a list of users, optionally notifying them why. // @Summary Enable/Disable a list of users, optionally notifying them why.
// @Produce json // @Produce json
// @Param enableDisableUserDTO body enableDisableUserDTO true "User enable/disable request object" // @Param enableDisableUserDTO body enableDisableUserDTO true "User enable/disable request object"
@ -705,6 +641,76 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
respondBool(200, true, gc) respondBool(200, true, gc)
} }
// @Summary Extend time before the user(s) expiry.
// @Produce json
// @Param extendExpiryDTO body extendExpiryDTO true "Extend expiry object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/extend [post]
// @tags Users
func (app *appContext) ExtendExpiry(gc *gin.Context) {
var req extendExpiryDTO
gc.BindJSON(&req)
app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users))
if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 {
respondBool(400, false, gc)
return
}
app.storage.usersLock.Lock()
defer app.storage.usersLock.Unlock()
for _, id := range req.Users {
if expiry, ok := app.storage.users[id]; ok {
app.storage.users[id] = expiry.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)
app.debug.Printf("Expiry extended for \"%s\"", id)
}
}
if err := app.storage.storeUsers(); err != nil {
app.err.Printf("Failed to store user duration: %v", err)
respondBool(500, false, gc)
return
}
respondBool(204, true, gc)
}
// @Summary Send an announcement via email to a given list of users.
// @Produce json
// @Param announcementDTO body announcementDTO true "Announcement request object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/announce [post]
// @Security Bearer
// @tags Users
func (app *appContext) Announce(gc *gin.Context) {
var req announcementDTO
gc.BindJSON(&req)
if !emailEnabled {
respondBool(400, false, gc)
return
}
addresses := []string{}
for _, userID := range req.Users {
addr, ok := app.storage.emails[userID]
if !ok || addr == "" {
continue
}
addresses = append(addresses, addr.(string))
}
msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
if err != nil {
app.err.Printf("Failed to construct announcement emails: %v", err)
respondBool(500, false, gc)
return
} else if err := app.email.send(msg, addresses...); err != nil {
app.err.Printf("Failed to send announcement emails: %v", err)
respondBool(500, false, gc)
return
}
app.info.Printf("Sent announcement email to %d users", len(addresses))
respondBool(200, true, gc)
}
// @Summary Create a new invite. // @Summary Create a new invite.
// @Produce json // @Produce json
// @Param generateInviteDTO body generateInviteDTO true "New invite request object" // @Param generateInviteDTO body generateInviteDTO true "New invite request object"
@ -775,6 +781,99 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
respondBool(200, true, gc) respondBool(200, true, gc)
} }
// @Summary Get invites.
// @Produce json
// @Success 200 {object} getInvitesDTO
// @Router /invites [get]
// @Security Bearer
// @tags Invites
func (app *appContext) GetInvites(gc *gin.Context) {
app.debug.Println("Invites requested")
currentTime := time.Now()
app.storage.loadInvites()
app.checkInvites()
var invites []inviteDTO
for code, inv := range app.storage.invites {
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
invite := inviteDTO{
Code: code,
Months: months,
Days: days,
Hours: hours,
Minutes: minutes,
UserExpiry: inv.UserExpiry,
UserMonths: inv.UserMonths,
UserDays: inv.UserDays,
UserHours: inv.UserHours,
UserMinutes: inv.UserMinutes,
Created: inv.Created.Unix(),
Profile: inv.Profile,
NoLimit: inv.NoLimit,
Label: inv.Label,
}
if len(inv.UsedBy) != 0 {
invite.UsedBy = map[string]int64{}
for _, pair := range inv.UsedBy {
// These used to be stored formatted instead of as a unix timestamp.
unix, err := strconv.ParseInt(pair[1], 10, 64)
if err != nil {
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
if err != nil {
app.err.Printf("Failed to parse usedBy time: %v", err)
}
unix = date.Unix()
}
invite.UsedBy[pair[0]] = unix
}
}
invite.RemainingUses = 1
if inv.RemainingUses != 0 {
invite.RemainingUses = inv.RemainingUses
}
if inv.Email != "" {
invite.Email = inv.Email
}
if len(inv.Notify) != 0 {
var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
app.storage.loadEmails()
if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
address = addr.(string)
}
} else {
address = app.config.Section("ui").Key("email").String()
}
if _, ok := inv.Notify[address]; ok {
if _, ok = inv.Notify[address]["notify-expiry"]; ok {
invite.NotifyExpiry = inv.Notify[address]["notify-expiry"]
}
if _, ok = inv.Notify[address]["notify-creation"]; ok {
invite.NotifyCreation = inv.Notify[address]["notify-creation"]
}
}
}
invites = append(invites, invite)
}
profiles := make([]string, len(app.storage.profiles))
if len(app.storage.profiles) != 0 {
profiles[0] = app.storage.defaultProfile
i := 1
if len(app.storage.profiles) > 1 {
for p := range app.storage.profiles {
if p != app.storage.defaultProfile {
profiles[i] = p
i++
}
}
}
}
resp := getInvitesDTO{
Profiles: profiles,
Invites: invites,
}
gc.JSON(200, resp)
}
// @Summary Set profile for an invite // @Summary Set profile for an invite
// @Produce json // @Produce json
// @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object" // @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object"
@ -913,99 +1012,6 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
respondBool(200, true, gc) respondBool(200, true, gc)
} }
// @Summary Get invites.
// @Produce json
// @Success 200 {object} getInvitesDTO
// @Router /invites [get]
// @Security Bearer
// @tags Invites
func (app *appContext) GetInvites(gc *gin.Context) {
app.debug.Println("Invites requested")
currentTime := time.Now()
app.storage.loadInvites()
app.checkInvites()
var invites []inviteDTO
for code, inv := range app.storage.invites {
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
invite := inviteDTO{
Code: code,
Months: months,
Days: days,
Hours: hours,
Minutes: minutes,
UserExpiry: inv.UserExpiry,
UserMonths: inv.UserMonths,
UserDays: inv.UserDays,
UserHours: inv.UserHours,
UserMinutes: inv.UserMinutes,
Created: inv.Created.Unix(),
Profile: inv.Profile,
NoLimit: inv.NoLimit,
Label: inv.Label,
}
if len(inv.UsedBy) != 0 {
invite.UsedBy = map[string]int64{}
for _, pair := range inv.UsedBy {
// These used to be stored formatted instead of as a unix timestamp.
unix, err := strconv.ParseInt(pair[1], 10, 64)
if err != nil {
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
if err != nil {
app.err.Printf("Failed to parse usedBy time: %v", err)
}
unix = date.Unix()
}
invite.UsedBy[pair[0]] = unix
}
}
invite.RemainingUses = 1
if inv.RemainingUses != 0 {
invite.RemainingUses = inv.RemainingUses
}
if inv.Email != "" {
invite.Email = inv.Email
}
if len(inv.Notify) != 0 {
var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
app.storage.loadEmails()
if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
address = addr.(string)
}
} else {
address = app.config.Section("ui").Key("email").String()
}
if _, ok := inv.Notify[address]; ok {
if _, ok = inv.Notify[address]["notify-expiry"]; ok {
invite.NotifyExpiry = inv.Notify[address]["notify-expiry"]
}
if _, ok = inv.Notify[address]["notify-creation"]; ok {
invite.NotifyCreation = inv.Notify[address]["notify-creation"]
}
}
}
invites = append(invites, invite)
}
profiles := make([]string, len(app.storage.profiles))
if len(app.storage.profiles) != 0 {
profiles[0] = app.storage.defaultProfile
i := 1
if len(app.storage.profiles) > 1 {
for p := range app.storage.profiles {
if p != app.storage.defaultProfile {
profiles[i] = p
i++
}
}
}
}
resp := getInvitesDTO{
Profiles: profiles,
Invites: invites,
}
gc.JSON(200, resp)
}
// @Summary Set notification preferences for an invite. // @Summary Set notification preferences for an invite.
// @Produce json // @Produce json
// @Param setNotifyDTO body setNotifyDTO true "Map of invite codes to notification settings objects" // @Param setNotifyDTO body setNotifyDTO true "Map of invite codes to notification settings objects"
@ -1433,7 +1439,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
// @Success 200 {object} emailListDTO // @Success 200 {object} emailListDTO
// @Router /config/emails [get] // @Router /config/emails [get]
// @tags Configuration // @tags Configuration
func (app *appContext) GetEmails(gc *gin.Context) { func (app *appContext) GetCustomEmails(gc *gin.Context) {
lang := gc.Query("lang") lang := gc.Query("lang")
if _, ok := app.storage.lang.Email[lang]; !ok { if _, ok := app.storage.lang.Email[lang]; !ok {
lang = app.storage.lang.chosenEmailLang lang = app.storage.lang.chosenEmailLang
@ -1458,9 +1464,10 @@ func (app *appContext) GetEmails(gc *gin.Context) {
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse // @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param id path string true "ID of email"
// @Router /config/emails/{id} [post] // @Router /config/emails/{id} [post]
// @tags Configuration // @tags Configuration
func (app *appContext) SetEmail(gc *gin.Context) { func (app *appContext) SetCustomEmail(gc *gin.Context) {
var req customEmail var req customEmail
gc.BindJSON(&req) gc.BindJSON(&req)
id := gc.Param("id") id := gc.Param("id")
@ -1515,9 +1522,11 @@ func (app *appContext) SetEmail(gc *gin.Context) {
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse // @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param enable/disable path string true "enable/disable"
// @Param id path string true "ID of email"
// @Router /config/emails/{id}/state/{enable/disable} [post] // @Router /config/emails/{id}/state/{enable/disable} [post]
// @tags Configuration // @tags Configuration
func (app *appContext) SetEmailState(gc *gin.Context) { func (app *appContext) SetCustomEmailState(gc *gin.Context) {
id := gc.Param("id") id := gc.Param("id")
s := gc.Param("state") s := gc.Param("state")
enabled := false enabled := false
@ -1563,9 +1572,10 @@ func (app *appContext) SetEmailState(gc *gin.Context) {
// @Success 200 {object} customEmailDTO // @Success 200 {object} customEmailDTO
// @Failure 400 {object} boolResponse // @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param id path string true "ID of email"
// @Router /config/emails/{id} [get] // @Router /config/emails/{id} [get]
// @tags Configuration // @tags Configuration
func (app *appContext) GetEmail(gc *gin.Context) { func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
lang := app.storage.lang.chosenEmailLang lang := app.storage.lang.chosenEmailLang
id := gc.Param("id") id := gc.Param("id")
var content string var content string
@ -1800,6 +1810,7 @@ func (app *appContext) Logout(gc *gin.Context) {
// @Produce json // @Produce json
// @Success 200 {object} langDTO // @Success 200 {object} langDTO
// @Failure 500 {object} stringResponse // @Failure 500 {object} stringResponse
// @Param page path string true "admin/form/setup/email/pwr"
// @Router /lang/{page} [get] // @Router /lang/{page} [get]
// @tags Other // @tags Other
func (app *appContext) GetLanguages(gc *gin.Context) { func (app *appContext) GetLanguages(gc *gin.Context) {
@ -1834,17 +1845,14 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
gc.JSON(200, resp) gc.JSON(200, resp)
} }
// @Summary Restarts the program. No response means success. // @Summary Serves a translations for pages "admin" or "form".
// @Router /restart [post] // @Produce json
// @Success 200 {object} adminLang
// @Failure 400 {object} boolResponse
// @Param page path string true "admin or form."
// @Param language path string true "language code, e.g en-us."
// @Router /lang/{page}/{language} [get]
// @tags Other // @tags Other
func (app *appContext) restart(gc *gin.Context) {
app.info.Println("Restarting...")
err := app.Restart()
if err != nil {
app.err.Printf("Couldn't restart, try restarting manually: %v", err)
}
}
func (app *appContext) ServeLang(gc *gin.Context) { func (app *appContext) ServeLang(gc *gin.Context) {
page := gc.Param("page") page := gc.Param("page")
lang := strings.Replace(gc.Param("file"), ".json", "", 1) lang := strings.Replace(gc.Param("file"), ".json", "", 1)
@ -1858,6 +1866,17 @@ func (app *appContext) ServeLang(gc *gin.Context) {
respondBool(400, false, gc) respondBool(400, false, gc)
} }
// @Summary Restarts the program. No response means success.
// @Router /restart [post]
// @tags Other
func (app *appContext) restart(gc *gin.Context) {
app.info.Println("Restarting...")
err := app.Restart()
if err != nil {
app.err.Printf("Couldn't restart, try restarting manually: %v", err)
}
}
// no need to syscall.exec anymore! // no need to syscall.exec anymore!
func (app *appContext) Restart() error { func (app *appContext) Restart() error {
RESTART <- true RESTART <- true

View File

@ -148,10 +148,10 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.POST(p+"/users/announce", app.Announce) api.POST(p+"/users/announce", app.Announce)
api.GET(p+"/config/update", app.CheckUpdate) api.GET(p+"/config/update", app.CheckUpdate)
api.POST(p+"/config/update", app.ApplyUpdate) api.POST(p+"/config/update", app.ApplyUpdate)
api.GET(p+"/config/emails", app.GetEmails) api.GET(p+"/config/emails", app.GetCustomEmails)
api.GET(p+"/config/emails/:id", app.GetEmail) api.GET(p+"/config/emails/:id", app.GetCustomEmailTemplate)
api.POST(p+"/config/emails/:id", app.SetEmail) api.POST(p+"/config/emails/:id", app.SetCustomEmail)
api.POST(p+"/config/emails/:id/state/:state", app.SetEmailState) api.POST(p+"/config/emails/:id/state/:state", app.SetCustomEmailState)
api.GET(p+"/config", app.GetConfig) api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart) api.POST(p+"/restart", app.restart)