2020-07-29 21:11:28 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-09-16 20:42:22 +00:00
|
|
|
"encoding/json"
|
2020-07-29 21:11:28 +00:00
|
|
|
"fmt"
|
2020-09-21 23:34:11 +00:00
|
|
|
"strconv"
|
2020-09-05 16:32:49 +00:00
|
|
|
"strings"
|
2020-07-29 21:11:28 +00:00
|
|
|
"time"
|
2020-08-16 12:36:54 +00:00
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/knz/strtime"
|
|
|
|
"github.com/lithammer/shortuuid/v3"
|
|
|
|
"gopkg.in/ini.v1"
|
2020-07-29 21:11:28 +00:00
|
|
|
)
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
func respond(code int, message string, gc *gin.Context) {
|
|
|
|
resp := stringResponse{}
|
|
|
|
if code == 200 || code == 204 {
|
|
|
|
resp.Response = message
|
|
|
|
} else {
|
|
|
|
resp.Error = message
|
|
|
|
}
|
|
|
|
gc.JSON(code, resp)
|
|
|
|
gc.Abort()
|
|
|
|
}
|
|
|
|
|
|
|
|
type stringResponse struct {
|
|
|
|
Response string `json:"response" example:"message"`
|
|
|
|
Error string `json:"error" example:"errorDescription"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type boolResponse struct {
|
|
|
|
Success bool `json:"success" example:"false"`
|
|
|
|
Error bool `json:"error" example:"true"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func respondBool(code int, val bool, gc *gin.Context) {
|
|
|
|
resp := boolResponse{}
|
|
|
|
if !val {
|
|
|
|
resp.Error = true
|
|
|
|
} else {
|
|
|
|
resp.Success = true
|
|
|
|
}
|
|
|
|
gc.JSON(code, resp)
|
|
|
|
gc.Abort()
|
|
|
|
}
|
|
|
|
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) loadStrftime() {
|
|
|
|
app.datePattern = app.config.Section("email").Key("date_format").String()
|
|
|
|
app.timePattern = `%H:%M`
|
|
|
|
if val, _ := app.config.Section("email").Key("use_24h").Bool(); !val {
|
|
|
|
app.timePattern = `%I:%M %p`
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) prettyTime(dt time.Time) (date, time string) {
|
|
|
|
date, _ = strtime.Strftime(dt, app.datePattern)
|
|
|
|
time, _ = strtime.Strftime(dt, app.timePattern)
|
2020-07-31 11:48:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) formatDatetime(dt time.Time) string {
|
|
|
|
d, t := app.prettyTime(dt)
|
2020-07-31 11:48:37 +00:00
|
|
|
return d + " " + t
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// https://stackoverflow.com/questions/36530251/time-since-with-months-and-years/36531443#36531443 THANKS
|
|
|
|
func timeDiff(a, b time.Time) (year, month, day, hour, min, sec int) {
|
|
|
|
if a.Location() != b.Location() {
|
|
|
|
b = b.In(a.Location())
|
|
|
|
}
|
|
|
|
if a.After(b) {
|
|
|
|
a, b = b, a
|
|
|
|
}
|
|
|
|
y1, M1, d1 := a.Date()
|
|
|
|
y2, M2, d2 := b.Date()
|
|
|
|
|
|
|
|
h1, m1, s1 := a.Clock()
|
|
|
|
h2, m2, s2 := b.Clock()
|
|
|
|
|
|
|
|
year = int(y2 - y1)
|
|
|
|
month = int(M2 - M1)
|
|
|
|
day = int(d2 - d1)
|
|
|
|
hour = int(h2 - h1)
|
|
|
|
min = int(m2 - m1)
|
|
|
|
sec = int(s2 - s1)
|
|
|
|
|
|
|
|
// Normalize negative values
|
|
|
|
if sec < 0 {
|
|
|
|
sec += 60
|
|
|
|
min--
|
|
|
|
}
|
|
|
|
if min < 0 {
|
|
|
|
min += 60
|
|
|
|
hour--
|
|
|
|
}
|
|
|
|
if hour < 0 {
|
|
|
|
hour += 24
|
|
|
|
day--
|
|
|
|
}
|
|
|
|
if day < 0 {
|
|
|
|
// days in month:
|
|
|
|
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
|
|
|
|
day += 32 - t.Day()
|
|
|
|
month--
|
|
|
|
}
|
|
|
|
if month < 0 {
|
|
|
|
month += 12
|
|
|
|
year--
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) checkInvites() {
|
2020-07-29 21:11:28 +00:00
|
|
|
current_time := time.Now()
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.loadInvites()
|
2020-07-29 21:11:28 +00:00
|
|
|
changed := false
|
2020-08-16 12:36:54 +00:00
|
|
|
for code, data := range app.storage.invites {
|
2020-07-29 21:11:28 +00:00
|
|
|
expiry := data.ValidTill
|
|
|
|
if current_time.After(expiry) {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
2020-07-29 21:11:28 +00:00
|
|
|
notify := data.Notify
|
2020-08-16 12:36:54 +00:00
|
|
|
if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
|
|
|
app.debug.Printf("%s: Expiry notification", code)
|
2020-07-31 11:48:37 +00:00
|
|
|
for address, settings := range notify {
|
|
|
|
if settings["notify-expiry"] {
|
2020-08-02 16:17:29 +00:00
|
|
|
go func() {
|
2020-09-13 20:18:47 +00:00
|
|
|
msg, err := app.email.constructExpiry(code, data, app)
|
|
|
|
if err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: Failed to construct expiry notification", code)
|
2020-09-13 20:07:15 +00:00
|
|
|
app.debug.Printf("Error: %s", err)
|
2020-09-13 20:18:47 +00:00
|
|
|
} else if err := app.email.send(address, msg); err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: Failed to send expiry notification", code)
|
2020-09-13 20:07:15 +00:00
|
|
|
app.debug.Printf("Error: %s", err)
|
2020-08-02 16:17:29 +00:00
|
|
|
} else {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.info.Printf("Sent expiry notification to %s", address)
|
2020-08-02 16:17:29 +00:00
|
|
|
}
|
|
|
|
}()
|
2020-07-31 11:48:37 +00:00
|
|
|
}
|
|
|
|
}
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
changed = true
|
2020-08-16 12:36:54 +00:00
|
|
|
delete(app.storage.invites, code)
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if changed {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.storeInvites()
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
2020-08-01 14:22:30 +00:00
|
|
|
}
|
|
|
|
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) checkInvite(code string, used bool, username string) bool {
|
2020-08-01 14:22:30 +00:00
|
|
|
current_time := time.Now()
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.loadInvites()
|
2020-08-01 14:22:30 +00:00
|
|
|
changed := false
|
2020-08-16 12:36:54 +00:00
|
|
|
if inv, match := app.storage.invites[code]; match {
|
2020-08-01 14:22:30 +00:00
|
|
|
expiry := inv.ValidTill
|
|
|
|
if current_time.After(expiry) {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
2020-08-01 14:22:30 +00:00
|
|
|
notify := inv.Notify
|
2020-08-16 12:36:54 +00:00
|
|
|
if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
|
|
|
app.debug.Printf("%s: Expiry notification", code)
|
2020-08-01 14:22:30 +00:00
|
|
|
for address, settings := range notify {
|
|
|
|
if settings["notify-expiry"] {
|
2020-08-02 16:17:29 +00:00
|
|
|
go func() {
|
2020-09-13 20:18:47 +00:00
|
|
|
msg, err := app.email.constructExpiry(code, inv, app)
|
|
|
|
if err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: Failed to construct expiry notification", code)
|
2020-09-13 20:07:15 +00:00
|
|
|
app.debug.Printf("Error: %s", err)
|
2020-09-13 20:18:47 +00:00
|
|
|
} else if err := app.email.send(address, msg); err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: Failed to send expiry notification", code)
|
2020-09-13 20:07:15 +00:00
|
|
|
app.debug.Printf("Error: %s", err)
|
2020-08-02 16:17:29 +00:00
|
|
|
} else {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.info.Printf("Sent expiry notification to %s", address)
|
2020-08-02 16:17:29 +00:00
|
|
|
}
|
|
|
|
}()
|
2020-08-01 14:22:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
changed = true
|
|
|
|
match = false
|
2020-08-16 12:36:54 +00:00
|
|
|
delete(app.storage.invites, code)
|
2020-08-01 14:22:30 +00:00
|
|
|
} else if used {
|
|
|
|
changed = true
|
|
|
|
del := false
|
|
|
|
newInv := inv
|
|
|
|
if newInv.RemainingUses == 1 {
|
|
|
|
del = true
|
2020-08-16 12:36:54 +00:00
|
|
|
delete(app.storage.invites, code)
|
2020-08-01 14:22:30 +00:00
|
|
|
} else if newInv.RemainingUses != 0 {
|
|
|
|
// 0 means infinite i guess?
|
|
|
|
newInv.RemainingUses -= 1
|
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
newInv.UsedBy = append(newInv.UsedBy, []string{username, app.formatDatetime(current_time)})
|
2020-08-01 14:22:30 +00:00
|
|
|
if !del {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.invites[code] = newInv
|
2020-08-01 14:22:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if changed {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.storeInvites()
|
2020-08-01 14:22:30 +00:00
|
|
|
}
|
|
|
|
return match
|
|
|
|
}
|
|
|
|
return false
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Routes from now on!
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type newUserDTO struct {
|
|
|
|
Username string `json:"username" example:"jeff" binding:"required"` // User's username
|
|
|
|
Password string `json:"password" example:"guest" binding:"required"` // User's password
|
|
|
|
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
|
|
|
|
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Creates a new Jellyfin user without an invite.
|
|
|
|
// @Produce json
|
|
|
|
// @Param newUserDTO body newUserDTO true "New user request object"
|
|
|
|
// @Success 200
|
|
|
|
// @Router /users [post]
|
2020-09-17 23:59:59 +00:00
|
|
|
func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
2020-09-24 16:51:13 +00:00
|
|
|
var req newUserDTO
|
2020-09-17 23:59:59 +00:00
|
|
|
gc.BindJSON(&req)
|
|
|
|
existingUser, _, _ := app.jf.userByName(req.Username, false)
|
|
|
|
if existingUser != nil {
|
|
|
|
msg := fmt.Sprintf("User already exists named %s", req.Username)
|
|
|
|
app.info.Printf("%s New user failed: %s", req.Username, msg)
|
|
|
|
respond(401, msg, gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
user, status, err := app.jf.newUser(req.Username, req.Password)
|
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
|
|
|
app.err.Printf("%s New user failed: Jellyfin responded with %d", req.Username, status)
|
|
|
|
respond(401, "Unknown error", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var id string
|
|
|
|
if user["Id"] != nil {
|
|
|
|
id = user["Id"].(string)
|
|
|
|
}
|
|
|
|
if len(app.storage.policy) != 0 {
|
|
|
|
status, err = app.jf.setPolicy(id, app.storage.policy)
|
|
|
|
if !(status == 200 || status == 204) {
|
|
|
|
app.err.Printf("%s: Failed to set user policy: Code %d", req.Username, status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 {
|
|
|
|
status, err = app.jf.setConfiguration(id, app.storage.configuration)
|
|
|
|
if (status == 200 || status == 204) && err == nil {
|
|
|
|
status, err = app.jf.setDisplayPreferences(id, app.storage.displayprefs)
|
|
|
|
} else {
|
|
|
|
app.err.Printf("%s: Failed to set configuration template: Code %d", req.Username, status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
|
|
|
app.storage.emails[id] = req.Email
|
|
|
|
app.storage.storeEmails()
|
|
|
|
}
|
|
|
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
|
|
|
app.storage.loadOmbiTemplate()
|
|
|
|
if len(app.storage.ombi_template) != 0 {
|
|
|
|
errors, code, err := app.ombi.newUser(req.Username, req.Password, req.Email, app.storage.ombi_template)
|
|
|
|
if err != nil || code != 200 {
|
|
|
|
app.info.Printf("Failed to create Ombi user (%d): %s", code, err)
|
|
|
|
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
|
|
|
|
} else {
|
|
|
|
app.info.Println("Created Ombi user")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-09-21 21:03:20 +00:00
|
|
|
app.jf.cacheExpiry = time.Now()
|
2020-09-17 23:59:59 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Creates a new Jellyfin user via invite code
|
|
|
|
// @Produce json
|
|
|
|
// @Param newUserDTO body newUserDTO true "New user request object"
|
|
|
|
// @Success 200 {object} PasswordValidation
|
|
|
|
// @Failure 400 {object} PasswordValidation
|
|
|
|
// @Router /newUser [post]
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) NewUser(gc *gin.Context) {
|
2020-09-24 16:51:13 +00:00
|
|
|
var req newUserDTO
|
2020-07-29 21:11:28 +00:00
|
|
|
gc.BindJSON(&req)
|
2020-08-16 12:36:54 +00:00
|
|
|
app.debug.Printf("%s: New user attempt", req.Code)
|
|
|
|
if !app.checkInvite(req.Code, false, "") {
|
|
|
|
app.info.Printf("%s New user failed: invalid code", req.Code)
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(401, false, gc)
|
2020-07-31 11:48:37 +00:00
|
|
|
return
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
validation := app.validator.validate(req.Password)
|
2020-07-29 21:11:28 +00:00
|
|
|
valid := true
|
|
|
|
for _, val := range validation {
|
|
|
|
if !val {
|
|
|
|
valid = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !valid {
|
|
|
|
// 200 bcs idk what i did in js
|
2020-08-16 12:36:54 +00:00
|
|
|
app.info.Printf("%s New user failed: Invalid password", req.Code)
|
2020-07-29 21:11:28 +00:00
|
|
|
gc.JSON(200, validation)
|
|
|
|
gc.Abort()
|
2020-07-31 11:48:37 +00:00
|
|
|
return
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
existingUser, _, _ := app.jf.userByName(req.Username, false)
|
2020-07-29 21:11:28 +00:00
|
|
|
if existingUser != nil {
|
2020-07-31 21:07:09 +00:00
|
|
|
msg := fmt.Sprintf("User already exists named %s", req.Username)
|
2020-08-16 12:36:54 +00:00
|
|
|
app.info.Printf("%s New user failed: %s", req.Code, msg)
|
2020-07-31 21:07:09 +00:00
|
|
|
respond(401, msg, gc)
|
2020-07-31 11:48:37 +00:00
|
|
|
return
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
user, status, err := app.jf.newUser(req.Username, req.Password)
|
2020-07-29 21:11:28 +00:00
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status)
|
2020-07-29 21:11:28 +00:00
|
|
|
respond(401, "Unknown error", gc)
|
2020-07-31 11:48:37 +00:00
|
|
|
return
|
|
|
|
}
|
2020-09-23 18:12:58 +00:00
|
|
|
app.storage.loadProfiles()
|
2020-08-16 12:36:54 +00:00
|
|
|
invite := app.storage.invites[req.Code]
|
2020-09-23 18:12:58 +00:00
|
|
|
app.checkInvite(req.Code, true, req.Username)
|
2020-08-16 12:36:54 +00:00
|
|
|
if app.config.Section("notifications").Key("enabled").MustBool(false) {
|
2020-07-31 11:48:37 +00:00
|
|
|
for address, settings := range invite.Notify {
|
|
|
|
if settings["notify-creation"] {
|
2020-08-02 16:17:29 +00:00
|
|
|
go func() {
|
2020-09-13 20:18:47 +00:00
|
|
|
msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app)
|
|
|
|
if err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: Failed to construct user creation notification", req.Code)
|
|
|
|
app.debug.Printf("%s: Error: %s", req.Code, err)
|
2020-09-13 20:18:47 +00:00
|
|
|
} else if err := app.email.send(address, msg); err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: Failed to send user creation notification", req.Code)
|
|
|
|
app.debug.Printf("%s: Error: %s", req.Code, err)
|
2020-08-02 16:17:29 +00:00
|
|
|
} else {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.info.Printf("%s: Sent user creation notification to %s", req.Code, address)
|
2020-08-02 16:17:29 +00:00
|
|
|
}
|
|
|
|
}()
|
2020-07-31 11:48:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var id string
|
|
|
|
if user["Id"] != nil {
|
|
|
|
id = user["Id"].(string)
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
2020-09-20 10:21:04 +00:00
|
|
|
if invite.Profile != "" {
|
2020-09-23 18:12:58 +00:00
|
|
|
app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile)
|
2020-09-20 10:21:04 +00:00
|
|
|
profile, ok := app.storage.profiles[invite.Profile]
|
|
|
|
if !ok {
|
|
|
|
profile = app.storage.profiles["Default"]
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
2020-09-20 10:21:04 +00:00
|
|
|
if len(profile.Policy) != 0 {
|
|
|
|
app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile)
|
|
|
|
status, err = app.jf.setPolicy(id, profile.Policy)
|
|
|
|
if !(status == 200 || status == 204) {
|
|
|
|
app.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(profile.Configuration) != 0 && len(profile.Displayprefs) != 0 {
|
|
|
|
app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile)
|
|
|
|
status, err = app.jf.setConfiguration(id, profile.Configuration)
|
|
|
|
if (status == 200 || status == 204) && err == nil {
|
|
|
|
status, err = app.jf.setDisplayPreferences(id, profile.Displayprefs)
|
|
|
|
} else {
|
|
|
|
app.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status)
|
|
|
|
}
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
|
|
|
app.storage.emails[id] = req.Email
|
|
|
|
app.storage.storeEmails()
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
2020-09-05 16:32:49 +00:00
|
|
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
|
|
|
app.storage.loadOmbiTemplate()
|
|
|
|
if len(app.storage.ombi_template) != 0 {
|
|
|
|
errors, code, err := app.ombi.newUser(req.Username, req.Password, req.Email, app.storage.ombi_template)
|
|
|
|
if err != nil || code != 200 {
|
|
|
|
app.info.Printf("Failed to create Ombi user (%d): %s", code, err)
|
|
|
|
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
|
|
|
|
} else {
|
|
|
|
app.info.Println("Created Ombi user")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-09-24 16:51:13 +00:00
|
|
|
code := 200
|
|
|
|
for _, val := range validation {
|
|
|
|
if !val {
|
|
|
|
code = 400
|
|
|
|
}
|
|
|
|
}
|
|
|
|
gc.JSON(code, validation)
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type deleteUserDTO struct {
|
|
|
|
Users []string `json:"users" binding:"required"` // List of usernames to delete
|
|
|
|
Notify bool `json:"notify"` // Whether to notify users of deletion
|
|
|
|
Reason string `json:"reason"` // Account deletion reason (for notification)
|
2020-09-17 22:50:07 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Delete a list of users, optionally notifying them why.
|
|
|
|
// @Produce json
|
|
|
|
// @Param deleteUserDTO body deleteUserDTO true "User deletion request object"
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Failure 400 {object} stringResponse
|
|
|
|
// @Failure 500 {object} errorListDTO "List of errors"
|
|
|
|
// @Router /users [delete]
|
2020-09-17 22:50:07 +00:00
|
|
|
func (app *appContext) DeleteUser(gc *gin.Context) {
|
2020-09-24 16:51:13 +00:00
|
|
|
var req deleteUserDTO
|
2020-09-17 22:50:07 +00:00
|
|
|
gc.BindJSON(&req)
|
|
|
|
errors := map[string]string{}
|
|
|
|
for _, userID := range req.Users {
|
|
|
|
status, err := app.jf.deleteUser(userID)
|
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
|
|
|
errors[userID] = fmt.Sprintf("%d: %s", status, err)
|
|
|
|
}
|
|
|
|
if req.Notify {
|
|
|
|
addr, ok := app.storage.emails[userID]
|
|
|
|
if addr != nil && ok {
|
|
|
|
go func(userID, reason, address string) {
|
|
|
|
msg, err := app.email.constructDeleted(reason, app)
|
|
|
|
if err != nil {
|
|
|
|
app.err.Printf("%s: Failed to construct account deletion email", userID)
|
|
|
|
app.debug.Printf("%s: Error: %s", userID, err)
|
|
|
|
} else if err := app.email.send(address, msg); err != nil {
|
|
|
|
app.err.Printf("%s: Failed to send to %s", userID, address)
|
|
|
|
app.debug.Printf("%s: Error: %s", userID, err)
|
|
|
|
} else {
|
|
|
|
app.info.Printf("%s: Sent invite email to %s", userID, address)
|
|
|
|
}
|
|
|
|
}(userID, req.Reason, addr.(string))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
app.jf.cacheExpiry = time.Now()
|
|
|
|
if len(errors) == len(req.Users) {
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(500, false, gc)
|
2020-09-17 22:50:07 +00:00
|
|
|
app.err.Printf("Account deletion failed: %s", errors[req.Users[0]])
|
|
|
|
return
|
|
|
|
} else if len(errors) != 0 {
|
|
|
|
gc.JSON(500, errors)
|
|
|
|
return
|
|
|
|
}
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(200, true, gc)
|
2020-09-17 22:50:07 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type generateInviteDTO struct {
|
|
|
|
Days int `json:"days" example:"1"` // Number of days
|
|
|
|
Hours int `json:"hours" example:"2"` // Number of hours
|
|
|
|
Minutes int `json:"minutes" example:"3"` // Number of minutes
|
|
|
|
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
|
|
|
|
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
|
|
|
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
|
|
|
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
|
|
|
Profile string `json:"profile" example:"DefaultProfile"` // Name of profile to apply on this invite
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Create a new invite.
|
|
|
|
// @Produce json
|
|
|
|
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Router /invites [post]
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
2020-09-24 16:51:13 +00:00
|
|
|
var req generateInviteDTO
|
2020-08-16 12:36:54 +00:00
|
|
|
app.debug.Println("Generating new invite")
|
|
|
|
app.storage.loadInvites()
|
2020-07-29 21:11:28 +00:00
|
|
|
gc.BindJSON(&req)
|
|
|
|
current_time := time.Now()
|
|
|
|
valid_till := current_time.AddDate(0, 0, req.Days)
|
|
|
|
valid_till = valid_till.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes))
|
2020-09-21 23:34:11 +00:00
|
|
|
// make sure code doesn't begin with number
|
2020-07-31 21:07:09 +00:00
|
|
|
invite_code := shortuuid.New()
|
2020-09-21 23:34:11 +00:00
|
|
|
_, err := strconv.Atoi(string(invite_code[0]))
|
|
|
|
for err == nil {
|
|
|
|
invite_code = shortuuid.New()
|
|
|
|
_, err = strconv.Atoi(string(invite_code[0]))
|
|
|
|
}
|
2020-07-29 21:11:28 +00:00
|
|
|
var invite Invite
|
2020-07-31 11:48:37 +00:00
|
|
|
invite.Created = current_time
|
2020-07-29 21:11:28 +00:00
|
|
|
if req.MultipleUses {
|
|
|
|
if req.NoLimit {
|
|
|
|
invite.NoLimit = true
|
|
|
|
} else {
|
|
|
|
invite.RemainingUses = req.RemainingUses
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
invite.RemainingUses = 1
|
|
|
|
}
|
|
|
|
invite.ValidTill = valid_till
|
2020-08-16 12:36:54 +00:00
|
|
|
if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
|
|
|
app.debug.Printf("%s: Sending invite email", invite_code)
|
2020-07-31 11:48:37 +00:00
|
|
|
invite.Email = req.Email
|
2020-09-13 20:18:47 +00:00
|
|
|
msg, err := app.email.constructInvite(invite_code, invite, app)
|
|
|
|
if err != nil {
|
2020-07-31 11:48:37 +00:00
|
|
|
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: Failed to construct invite email", invite_code)
|
|
|
|
app.debug.Printf("%s: Error: %s", invite_code, err)
|
2020-09-13 20:18:47 +00:00
|
|
|
} else if err := app.email.send(req.Email, msg); err != nil {
|
2020-07-31 11:48:37 +00:00
|
|
|
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: %s", invite_code, invite.Email)
|
|
|
|
app.debug.Printf("%s: Error: %s", invite_code, err)
|
2020-07-31 21:07:09 +00:00
|
|
|
} else {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.info.Printf("%s: Sent invite email to %s", invite_code, req.Email)
|
2020-07-31 11:48:37 +00:00
|
|
|
}
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
2020-09-20 10:21:04 +00:00
|
|
|
if req.Profile != "" {
|
|
|
|
if _, ok := app.storage.profiles[req.Profile]; ok {
|
|
|
|
invite.Profile = req.Profile
|
|
|
|
} else {
|
|
|
|
invite.Profile = "Default"
|
|
|
|
}
|
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.invites[invite_code] = invite
|
|
|
|
app.storage.storeInvites()
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(200, true, gc)
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type inviteProfileDTO struct {
|
|
|
|
Invite string `json:"invite" example:"slakdaslkdl2342"` // Invite to apply to
|
|
|
|
Profile string `json:"profile" example:"DefaultProfile"` // Profile to use
|
2020-09-20 10:21:04 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Set profile for an invite
|
|
|
|
// @Produce json
|
|
|
|
// @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object"
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Failure 500 {object} stringResponse
|
|
|
|
// @Router /invites/profile [post]
|
2020-09-20 10:21:04 +00:00
|
|
|
func (app *appContext) SetProfile(gc *gin.Context) {
|
2020-09-24 16:51:13 +00:00
|
|
|
var req inviteProfileDTO
|
2020-09-20 10:21:04 +00:00
|
|
|
gc.BindJSON(&req)
|
|
|
|
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
|
2020-09-21 23:34:11 +00:00
|
|
|
// "" means "Don't apply profile"
|
|
|
|
if _, ok := app.storage.profiles[req.Profile]; !ok && req.Profile != "" {
|
2020-09-20 10:21:04 +00:00
|
|
|
app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile)
|
|
|
|
respond(500, "Profile not found", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
inv := app.storage.invites[req.Invite]
|
|
|
|
inv.Profile = req.Profile
|
|
|
|
app.storage.invites[req.Invite] = inv
|
|
|
|
app.storage.storeInvites()
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(200, true, gc)
|
2020-09-20 10:21:04 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type profileDTO struct {
|
|
|
|
Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
|
|
|
|
LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
|
|
|
|
FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
|
|
|
|
}
|
|
|
|
|
|
|
|
type getProfilesDTO struct {
|
|
|
|
Profiles map[string]profileDTO `json:"profiles"`
|
|
|
|
DefaultProfile string `json:"default_profile"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// @Summary Get a list of profiles
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} getProfilesDTO
|
|
|
|
// @Router /profiles [get]
|
2020-09-22 23:01:07 +00:00
|
|
|
func (app *appContext) GetProfiles(gc *gin.Context) {
|
|
|
|
app.storage.loadProfiles()
|
|
|
|
app.debug.Println("Profiles requested")
|
2020-09-24 16:51:13 +00:00
|
|
|
out := getProfilesDTO{
|
|
|
|
DefaultProfile: app.storage.defaultProfile,
|
|
|
|
Profiles: map[string]profileDTO{},
|
2020-09-23 17:48:00 +00:00
|
|
|
}
|
2020-09-22 23:01:07 +00:00
|
|
|
for name, p := range app.storage.profiles {
|
2020-09-24 16:51:13 +00:00
|
|
|
out.Profiles[name] = profileDTO{
|
|
|
|
Admin: p.Admin,
|
|
|
|
LibraryAccess: p.LibraryAccess,
|
|
|
|
FromUser: p.FromUser,
|
2020-09-22 23:01:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
gc.JSON(200, out)
|
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type profileChangeDTO struct {
|
|
|
|
Name string `json:"name" example:"DefaultProfile" binding:"required"` // Name of the profile
|
|
|
|
}
|
|
|
|
|
|
|
|
// @Summary Set the default profile to use.
|
|
|
|
// @Produce json
|
|
|
|
// @Param profileChangeDTO body profileChangeDTO true "Default profile object"
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Failure 500 {object} stringResponse
|
|
|
|
// @Router /profiles/default [post]
|
2020-09-23 17:48:00 +00:00
|
|
|
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
2020-09-24 16:51:13 +00:00
|
|
|
req := profileChangeDTO{}
|
2020-09-23 17:48:00 +00:00
|
|
|
gc.BindJSON(&req)
|
2020-09-24 16:51:13 +00:00
|
|
|
app.info.Printf("Setting default profile to \"%s\"", req.Name)
|
|
|
|
if _, ok := app.storage.profiles[req.Name]; !ok {
|
|
|
|
app.err.Printf("Profile not found: \"%s\"", req.Name)
|
2020-09-23 17:48:00 +00:00
|
|
|
respond(500, "Profile not found", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for name, profile := range app.storage.profiles {
|
2020-09-24 16:51:13 +00:00
|
|
|
if name == req.Name {
|
2020-09-23 17:48:00 +00:00
|
|
|
profile.Admin = true
|
|
|
|
app.storage.profiles[name] = profile
|
|
|
|
} else {
|
|
|
|
profile.Admin = false
|
|
|
|
}
|
|
|
|
}
|
2020-09-24 16:51:13 +00:00
|
|
|
app.storage.defaultProfile = req.Name
|
|
|
|
respondBool(200, true, gc)
|
2020-09-23 17:48:00 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type newProfileDTO struct {
|
|
|
|
Name string `json:"name" example:"DefaultProfile" binding:"required"` // Name of the profile
|
|
|
|
ID string `json:"id" example:"kasdjlaskjd342342" binding:"required"` // ID of user to source settings from
|
|
|
|
Homescreen bool `json:"homescreen" example:"true"` // Whether to store homescreen layout or not
|
2020-09-22 23:01:07 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Create a profile based on a Jellyfin user's settings.
|
|
|
|
// @Produce json
|
|
|
|
// @Param newProfileDTO body newProfileDTO true "New profile object"
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Failure 500 {object} stringResponse
|
|
|
|
// @Router /profiles [post]
|
2020-09-22 23:01:07 +00:00
|
|
|
func (app *appContext) CreateProfile(gc *gin.Context) {
|
|
|
|
fmt.Println("Profile creation requested")
|
2020-09-24 16:51:13 +00:00
|
|
|
var req newProfileDTO
|
2020-09-22 23:01:07 +00:00
|
|
|
gc.BindJSON(&req)
|
|
|
|
user, status, err := app.jf.userById(req.ID, false)
|
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
|
|
|
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
|
|
|
app.debug.Printf("Error: %s", err)
|
|
|
|
respond(500, "Couldn't get user", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
profile := Profile{
|
|
|
|
FromUser: user["Name"].(string),
|
|
|
|
Policy: user["Policy"].(map[string]interface{}),
|
|
|
|
}
|
|
|
|
app.debug.Printf("Creating profile from user \"%s\"", user["Name"].(string))
|
|
|
|
if req.Homescreen {
|
|
|
|
profile.Configuration = user["Configuration"].(map[string]interface{})
|
|
|
|
profile.Displayprefs, status, err = app.jf.getDisplayPreferences(req.ID)
|
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
|
|
|
app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
|
|
|
|
app.debug.Printf("Error: %s", err)
|
|
|
|
respond(500, "Couldn't get displayprefs", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
app.storage.loadProfiles()
|
|
|
|
app.storage.profiles[req.Name] = profile
|
|
|
|
app.storage.storeProfiles()
|
|
|
|
app.storage.loadProfiles()
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(200, true, gc)
|
2020-09-22 23:01:07 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Delete an existing profile
|
|
|
|
// @Produce json
|
|
|
|
// @Param profileChangeDTO body profileChangeDTO true "Delete profile object"
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Router /profiles [delete]
|
2020-09-22 23:01:07 +00:00
|
|
|
func (app *appContext) DeleteProfile(gc *gin.Context) {
|
2020-09-24 16:51:13 +00:00
|
|
|
req := profileChangeDTO{}
|
2020-09-22 23:01:07 +00:00
|
|
|
gc.BindJSON(&req)
|
2020-09-24 16:51:13 +00:00
|
|
|
name := req.Name
|
2020-09-22 23:01:07 +00:00
|
|
|
if _, ok := app.storage.profiles[name]; ok {
|
|
|
|
delete(app.storage.profiles, name)
|
|
|
|
}
|
|
|
|
app.storage.storeProfiles()
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(200, true, gc)
|
|
|
|
}
|
|
|
|
|
|
|
|
type inviteDTO struct {
|
|
|
|
Code string `json:"code" example:"sajdlj23423j23"` // Invite code
|
|
|
|
Days int `json:"days" example:"1"` // Number of days till expiry
|
|
|
|
Hours int `json:"hours" example:"2"` // Number of hours till expiry
|
|
|
|
Minutes int `json:"minutes" example:"3"` // Number of minutes till expiry
|
|
|
|
Created string `json:"created" example:"01/01/20 12:00"` // Date of creation
|
|
|
|
Profile string `json:"profile" example:"DefaultProfile"` // Profile used on this invite
|
|
|
|
UsedBy [][]string `json:"used-by,omitempty"` // Users who have used this invite
|
|
|
|
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
|
|
|
|
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
|
|
|
|
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable)
|
|
|
|
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
|
|
|
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
2020-09-22 23:01:07 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type getInvitesDTO struct {
|
|
|
|
Profiles []string `json:"profiles"` // List of profiles (name only)
|
|
|
|
Invites []inviteDTO `json:"invites"` // List of invites
|
|
|
|
}
|
|
|
|
|
|
|
|
// @Summary Get invites.
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} getInvitesDTO
|
|
|
|
// @Router /invites [get]
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) GetInvites(gc *gin.Context) {
|
|
|
|
app.debug.Println("Invites requested")
|
2020-07-29 21:11:28 +00:00
|
|
|
current_time := time.Now()
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.loadInvites()
|
|
|
|
app.checkInvites()
|
2020-09-24 16:51:13 +00:00
|
|
|
var invites []inviteDTO
|
2020-08-16 12:36:54 +00:00
|
|
|
for code, inv := range app.storage.invites {
|
2020-07-29 21:11:28 +00:00
|
|
|
_, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time)
|
2020-09-24 16:51:13 +00:00
|
|
|
invite := inviteDTO{
|
|
|
|
Code: code,
|
|
|
|
Days: days,
|
|
|
|
Hours: hours,
|
|
|
|
Minutes: minutes,
|
|
|
|
Created: app.formatDatetime(inv.Created),
|
|
|
|
Profile: inv.Profile,
|
|
|
|
NoLimit: inv.NoLimit,
|
2020-09-20 10:21:04 +00:00
|
|
|
}
|
2020-07-29 21:11:28 +00:00
|
|
|
if len(inv.UsedBy) != 0 {
|
2020-09-24 16:51:13 +00:00
|
|
|
invite.UsedBy = inv.UsedBy
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
2020-09-24 16:51:13 +00:00
|
|
|
invite.RemainingUses = 1
|
2020-07-29 21:11:28 +00:00
|
|
|
if inv.RemainingUses != 0 {
|
2020-09-24 16:51:13 +00:00
|
|
|
invite.RemainingUses = inv.RemainingUses
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
if inv.Email != "" {
|
2020-09-24 16:51:13 +00:00
|
|
|
invite.Email = inv.Email
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
if len(inv.Notify) != 0 {
|
|
|
|
var address string
|
2020-08-16 12:36:54 +00:00
|
|
|
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
|
|
|
app.storage.loadEmails()
|
2020-09-19 08:09:30 +00:00
|
|
|
if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
|
|
|
|
address = addr.(string)
|
|
|
|
}
|
2020-07-29 21:11:28 +00:00
|
|
|
} else {
|
2020-08-16 12:36:54 +00:00
|
|
|
address = app.config.Section("ui").Key("email").String()
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
if _, ok := inv.Notify[address]; ok {
|
2020-09-24 16:51:13 +00:00
|
|
|
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"]
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
invites = append(invites, invite)
|
|
|
|
}
|
2020-09-20 10:21:04 +00:00
|
|
|
profiles := make([]string, len(app.storage.profiles))
|
2020-09-23 17:48:00 +00:00
|
|
|
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++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-09-20 10:21:04 +00:00
|
|
|
}
|
2020-09-24 16:51:13 +00:00
|
|
|
resp := getInvitesDTO{
|
|
|
|
Profiles: profiles,
|
|
|
|
Invites: invites,
|
2020-07-29 21:11:28 +00:00
|
|
|
}
|
|
|
|
gc.JSON(200, resp)
|
|
|
|
}
|
2020-07-31 11:48:37 +00:00
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// fake DTO, if i actually used this the code would be a lot longer
|
|
|
|
type setNotifyValues map[string]struct {
|
|
|
|
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
|
|
|
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
|
|
|
}
|
|
|
|
|
|
|
|
type setNotifyDTO map[string]setNotifyValues
|
|
|
|
|
|
|
|
// @Summary Set notification preferences for an invite.
|
|
|
|
// @Produce json
|
|
|
|
// @Param setNotifyDTO body setNotifyDTO true "Map of invite codes to notification settings objects"
|
|
|
|
// @Success 200
|
|
|
|
// @Failure 400 {object} stringResponse
|
|
|
|
// @Failure 500 {object} stringResponse
|
|
|
|
// @Router /invites/notify [post]
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) SetNotify(gc *gin.Context) {
|
2020-09-19 16:05:09 +00:00
|
|
|
var req map[string]map[string]bool
|
2020-07-31 11:48:37 +00:00
|
|
|
gc.BindJSON(&req)
|
|
|
|
changed := false
|
|
|
|
for code, settings := range req {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.debug.Printf("%s: Notification settings change requested", code)
|
|
|
|
app.storage.loadInvites()
|
|
|
|
app.storage.loadEmails()
|
|
|
|
invite, ok := app.storage.invites[code]
|
2020-07-31 11:48:37 +00:00
|
|
|
if !ok {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
2020-09-24 16:51:13 +00:00
|
|
|
respond(400, "Invalid invite code", gc)
|
2020-07-31 11:48:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
var address string
|
2020-08-16 12:36:54 +00:00
|
|
|
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
2020-07-31 11:48:37 +00:00
|
|
|
var ok bool
|
2020-08-16 12:36:54 +00:00
|
|
|
address, ok = app.storage.emails[gc.GetString("jfId")].(string)
|
2020-07-31 11:48:37 +00:00
|
|
|
if !ok {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
|
|
|
|
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
2020-09-24 16:51:13 +00:00
|
|
|
respond(500, "Missing user email", gc)
|
2020-07-31 11:48:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
2020-08-16 12:36:54 +00:00
|
|
|
address = app.config.Section("ui").Key("email").String()
|
2020-07-31 11:48:37 +00:00
|
|
|
}
|
|
|
|
if invite.Notify == nil {
|
|
|
|
invite.Notify = map[string]map[string]bool{}
|
|
|
|
}
|
|
|
|
if _, ok := invite.Notify[address]; !ok {
|
|
|
|
invite.Notify[address] = map[string]bool{}
|
|
|
|
} /*else {
|
|
|
|
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
|
|
|
|
*/
|
2020-09-19 16:05:09 +00:00
|
|
|
if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
|
|
|
|
invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
|
|
|
|
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
|
2020-07-31 11:48:37 +00:00
|
|
|
changed = true
|
|
|
|
}
|
2020-09-19 16:05:09 +00:00
|
|
|
if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] {
|
|
|
|
invite.Notify[address]["notify-creation"] = settings["notify-creation"]
|
|
|
|
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address)
|
2020-07-31 11:48:37 +00:00
|
|
|
changed = true
|
|
|
|
}
|
|
|
|
if changed {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.invites[code] = invite
|
2020-07-31 11:48:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if changed {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.storeInvites()
|
2020-07-31 11:48:37 +00:00
|
|
|
}
|
|
|
|
}
|
2020-07-31 12:59:25 +00:00
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type deleteInviteDTO struct {
|
|
|
|
Code string `json:"code" example:"skjadajd43234s"` // Code of invite to delete
|
2020-07-31 12:59:25 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Delete an invite.
|
|
|
|
// @Produce json
|
|
|
|
// @Param deleteInviteDTO body deleteInviteDTO true "Delete invite object"
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Failure 400 {object} stringResponse
|
|
|
|
// @Router /invites [delete]
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) DeleteInvite(gc *gin.Context) {
|
2020-09-24 16:51:13 +00:00
|
|
|
var req deleteInviteDTO
|
2020-07-31 12:59:25 +00:00
|
|
|
gc.BindJSON(&req)
|
2020-08-16 12:36:54 +00:00
|
|
|
app.debug.Printf("%s: Deletion requested", req.Code)
|
2020-07-31 12:59:25 +00:00
|
|
|
var ok bool
|
2020-08-16 12:36:54 +00:00
|
|
|
_, ok = app.storage.invites[req.Code]
|
2020-07-31 12:59:25 +00:00
|
|
|
if ok {
|
2020-08-16 12:36:54 +00:00
|
|
|
delete(app.storage.invites, req.Code)
|
|
|
|
app.storage.storeInvites()
|
|
|
|
app.info.Printf("%s: Invite deleted", req.Code)
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(200, true, gc)
|
2020-07-31 12:59:25 +00:00
|
|
|
return
|
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
|
2020-09-24 16:51:13 +00:00
|
|
|
respond(400, "Code doesn't exist", gc)
|
2020-07-31 12:59:25 +00:00
|
|
|
}
|
|
|
|
|
2020-09-16 20:42:22 +00:00
|
|
|
type dateToParse struct {
|
|
|
|
Parsed time.Time `json:"parseme"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// json magically parses datetimes so why not
|
|
|
|
func parseDt(date string) time.Time {
|
|
|
|
timeJSON := []byte("{ \"parseme\": \"" + date + "\" }")
|
|
|
|
var parsed dateToParse
|
|
|
|
json.Unmarshal(timeJSON, &parsed)
|
|
|
|
return parsed.Parsed
|
2020-07-31 12:59:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type respUser struct {
|
2020-09-24 16:51:13 +00:00
|
|
|
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
|
|
|
|
Name string `json:"name" example:"jeff"` // Username of user
|
|
|
|
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
|
|
|
|
LastActive string `json:"last_active"` // Time of last activity on Jellyfin
|
|
|
|
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
2020-09-16 20:42:22 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type getUsersDTO struct {
|
2020-09-16 20:42:22 +00:00
|
|
|
UserList []respUser `json:"users"`
|
2020-07-31 12:59:25 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Get a list of Jellyfin users.
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} getUsersDTO
|
|
|
|
// @Failure 500 {object} stringResponse
|
|
|
|
// @Router /users [get]
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) GetUsers(gc *gin.Context) {
|
|
|
|
app.debug.Println("Users requested")
|
2020-09-24 16:51:13 +00:00
|
|
|
var resp getUsersDTO
|
2020-07-31 12:59:25 +00:00
|
|
|
resp.UserList = []respUser{}
|
2020-08-16 12:36:54 +00:00
|
|
|
users, status, err := app.jf.getUsers(false)
|
2020-07-31 12:59:25 +00:00
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
|
|
|
app.debug.Printf("Error: %s", err)
|
2020-07-31 12:59:25 +00:00
|
|
|
respond(500, "Couldn't get users", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for _, jfUser := range users {
|
|
|
|
var user respUser
|
2020-09-16 20:42:22 +00:00
|
|
|
user.LastActive = "n/a"
|
|
|
|
if jfUser["LastActivityDate"] != nil {
|
|
|
|
date := parseDt(jfUser["LastActivityDate"].(string))
|
|
|
|
user.LastActive = app.formatDatetime(date)
|
|
|
|
}
|
|
|
|
user.ID = jfUser["Id"].(string)
|
2020-07-31 12:59:25 +00:00
|
|
|
user.Name = jfUser["Name"].(string)
|
2020-09-16 20:42:22 +00:00
|
|
|
user.Admin = jfUser["Policy"].(map[string]interface{})["IsAdministrator"].(bool)
|
2020-08-16 12:36:54 +00:00
|
|
|
if email, ok := app.storage.emails[jfUser["Id"].(string)]; ok {
|
2020-07-31 12:59:25 +00:00
|
|
|
user.Email = email.(string)
|
|
|
|
}
|
2020-09-16 20:42:22 +00:00
|
|
|
|
2020-07-31 12:59:25 +00:00
|
|
|
resp.UserList = append(resp.UserList, user)
|
|
|
|
}
|
|
|
|
gc.JSON(200, resp)
|
|
|
|
}
|
|
|
|
|
2020-09-05 16:32:49 +00:00
|
|
|
type ombiUser struct {
|
2020-09-24 16:51:13 +00:00
|
|
|
Name string `json:"name,omitempty" example:"jeff"` // Name of Ombi user
|
|
|
|
ID string `json:"id" example:"djgkjdg7dkjfsj8"` // userID of Ombi user
|
2020-09-05 16:32:49 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type ombiUsersDTO struct {
|
|
|
|
Users []ombiUser `json:"users"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// @Summary Get a list of Ombi users.
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} ombiUsersDTO
|
|
|
|
// @Failure 500 {object} stringResponse
|
|
|
|
// @Router /ombi/users [get]
|
2020-09-05 16:32:49 +00:00
|
|
|
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
|
|
|
app.debug.Println("Ombi users requested")
|
|
|
|
users, status, err := app.ombi.getUsers()
|
|
|
|
if err != nil || status != 200 {
|
|
|
|
app.err.Printf("Failed to get users from Ombi: Code %d", status)
|
|
|
|
app.debug.Printf("Error: %s", err)
|
|
|
|
respond(500, "Couldn't get users", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
userlist := make([]ombiUser, len(users))
|
|
|
|
for i, data := range users {
|
|
|
|
userlist[i] = ombiUser{
|
|
|
|
Name: data["userName"].(string),
|
|
|
|
ID: data["id"].(string),
|
|
|
|
}
|
|
|
|
}
|
2020-09-24 16:51:13 +00:00
|
|
|
gc.JSON(200, ombiUsersDTO{Users: userlist})
|
2020-09-05 16:32:49 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Set new user defaults for Ombi accounts.
|
|
|
|
// @Produce json
|
|
|
|
// @Param ombiUser body ombiUser true "User to source settings from"
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Failure 500 {object} stringResponse
|
|
|
|
// @Router /ombi/defaults [post]
|
|
|
|
func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
|
|
|
|
var req ombiUser
|
|
|
|
gc.BindJSON(&req)
|
|
|
|
template, code, err := app.ombi.templateByID(req.ID)
|
|
|
|
if err != nil || code != 200 || len(template) == 0 {
|
|
|
|
app.err.Printf("Couldn't get user from Ombi: %d %s", code, err)
|
|
|
|
respond(500, "Couldn't get user", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
app.storage.ombi_template = template
|
|
|
|
fmt.Println(app.storage.ombi_path)
|
|
|
|
app.storage.storeOmbiTemplate()
|
|
|
|
respondBool(200, true, gc)
|
|
|
|
}
|
|
|
|
|
|
|
|
type modifyEmailsDTO map[string]string
|
|
|
|
|
|
|
|
// @Summary Modify user's email addresses.
|
|
|
|
// @Produce json
|
|
|
|
// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses"
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Failure 500 {object} stringResponse
|
|
|
|
// @Router /users/emails [post]
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) ModifyEmails(gc *gin.Context) {
|
2020-09-24 16:51:13 +00:00
|
|
|
var req modifyEmailsDTO
|
2020-07-31 12:59:25 +00:00
|
|
|
gc.BindJSON(&req)
|
2020-09-17 20:23:45 +00:00
|
|
|
fmt.Println(req)
|
2020-08-16 12:36:54 +00:00
|
|
|
app.debug.Println("Email modification requested")
|
|
|
|
users, status, err := app.jf.getUsers(false)
|
2020-07-31 12:59:25 +00:00
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
|
|
|
app.debug.Printf("Error: %s", err)
|
2020-07-31 12:59:25 +00:00
|
|
|
respond(500, "Couldn't get users", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for _, jfUser := range users {
|
2020-09-17 20:23:45 +00:00
|
|
|
if address, ok := req[jfUser["Id"].(string)]; ok {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.emails[jfUser["Id"].(string)] = address
|
2020-07-31 12:59:25 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.storeEmails()
|
|
|
|
app.info.Println("Email list modified")
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(200, true, gc)
|
2020-07-31 12:59:25 +00:00
|
|
|
}
|
|
|
|
|
2020-09-22 23:01:07 +00:00
|
|
|
/*func (app *appContext) SetDefaults(gc *gin.Context) {
|
2020-07-31 12:59:25 +00:00
|
|
|
var req defaultsReq
|
|
|
|
gc.BindJSON(&req)
|
2020-09-17 15:51:19 +00:00
|
|
|
userID := req.ID
|
|
|
|
user, status, err := app.jf.userById(userID, false)
|
2020-07-31 12:59:25 +00:00
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
|
|
|
app.debug.Printf("Error: %s", err)
|
2020-07-31 12:59:25 +00:00
|
|
|
respond(500, "Couldn't get user", gc)
|
|
|
|
return
|
|
|
|
}
|
2020-09-17 15:51:19 +00:00
|
|
|
app.info.Printf("Getting user defaults from \"%s\"", user["Name"].(string))
|
2020-07-31 12:59:25 +00:00
|
|
|
policy := user["Policy"].(map[string]interface{})
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.policy = policy
|
|
|
|
app.storage.storePolicy()
|
|
|
|
app.debug.Println("User policy template stored")
|
2020-07-31 12:59:25 +00:00
|
|
|
if req.Homescreen {
|
|
|
|
configuration := user["Configuration"].(map[string]interface{})
|
|
|
|
var displayprefs map[string]interface{}
|
2020-09-13 20:07:15 +00:00
|
|
|
displayprefs, status, err = app.jf.getDisplayPreferences(userID)
|
2020-07-31 12:59:25 +00:00
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
|
|
|
|
app.debug.Printf("Error: %s", err)
|
2020-07-31 12:59:25 +00:00
|
|
|
respond(500, "Couldn't get displayprefs", gc)
|
|
|
|
return
|
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
app.storage.configuration = configuration
|
|
|
|
app.storage.displayprefs = displayprefs
|
|
|
|
app.storage.storeConfiguration()
|
|
|
|
app.debug.Println("Configuration template stored")
|
|
|
|
app.storage.storeDisplayprefs()
|
|
|
|
app.debug.Println("DisplayPrefs template stored")
|
2020-07-31 12:59:25 +00:00
|
|
|
}
|
|
|
|
gc.JSON(200, map[string]bool{"success": true})
|
2020-09-22 23:01:07 +00:00
|
|
|
}*/
|
2020-07-31 15:09:30 +00:00
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
type userSettingsDTO struct {
|
|
|
|
From string `json:"from"` // Whether to apply from "user" or "profile"
|
|
|
|
Profile string `json:"profile"` // Name of profile (if from = "profile")
|
|
|
|
ApplyTo []string `json:"apply_to"` // Users to apply settings to
|
|
|
|
ID string `json:"id"` // ID of user (if from = "user")
|
|
|
|
Homescreen bool `json:"homescreen"` // Whether to apply homescreen layout or not
|
|
|
|
}
|
|
|
|
|
|
|
|
type errorListDTO map[string]map[string]string
|
|
|
|
|
|
|
|
// @Summary Apply settings to a list of users, either from a profile or from another user.
|
|
|
|
// @Produce json
|
|
|
|
// @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings"
|
|
|
|
// @Success 200 {object} errorListDTO
|
|
|
|
// @Failure 500 {object} errorListDTO "Lists of errors that occured while applying settings"
|
|
|
|
// @Router /users/settings [post]
|
2020-09-17 15:51:19 +00:00
|
|
|
func (app *appContext) ApplySettings(gc *gin.Context) {
|
2020-09-22 23:01:07 +00:00
|
|
|
app.info.Println("User settings change requested")
|
2020-09-24 16:51:13 +00:00
|
|
|
var req userSettingsDTO
|
2020-09-17 15:51:19 +00:00
|
|
|
gc.BindJSON(&req)
|
2020-09-22 23:01:07 +00:00
|
|
|
applyingFrom := "profile"
|
2020-09-17 15:51:19 +00:00
|
|
|
var policy, configuration, displayprefs map[string]interface{}
|
2020-09-22 23:01:07 +00:00
|
|
|
if req.From == "profile" {
|
|
|
|
app.storage.loadProfiles()
|
|
|
|
if _, ok := app.storage.profiles[req.Profile]; !ok || len(app.storage.profiles[req.Profile].Policy) == 0 {
|
|
|
|
app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile)
|
|
|
|
respond(500, "Couldn't find profile", gc)
|
2020-09-17 15:51:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if req.Homescreen {
|
2020-09-22 23:01:07 +00:00
|
|
|
if len(app.storage.profiles[req.Profile].Configuration) == 0 || len(app.storage.profiles[req.Profile].Displayprefs) == 0 {
|
|
|
|
app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile)
|
2020-09-17 15:51:19 +00:00
|
|
|
respond(500, "No homescreen template available", gc)
|
|
|
|
return
|
|
|
|
}
|
2020-09-22 23:01:07 +00:00
|
|
|
configuration = app.storage.profiles[req.Profile].Configuration
|
|
|
|
displayprefs = app.storage.profiles[req.Profile].Displayprefs
|
2020-09-17 15:51:19 +00:00
|
|
|
}
|
2020-09-22 23:01:07 +00:00
|
|
|
policy = app.storage.profiles[req.Profile].Policy
|
2020-09-17 15:51:19 +00:00
|
|
|
} else if req.From == "user" {
|
|
|
|
applyingFrom = "user"
|
|
|
|
user, status, err := app.jf.userById(req.ID, false)
|
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
|
|
|
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
|
|
|
app.debug.Printf("Error: %s", err)
|
|
|
|
respond(500, "Couldn't get user", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
applyingFrom = "\"" + user["Name"].(string) + "\""
|
|
|
|
policy = user["Policy"].(map[string]interface{})
|
|
|
|
if req.Homescreen {
|
|
|
|
displayprefs, status, err = app.jf.getDisplayPreferences(req.ID)
|
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
|
|
|
app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
|
|
|
|
app.debug.Printf("Error: %s", err)
|
|
|
|
respond(500, "Couldn't get displayprefs", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
configuration = user["Configuration"].(map[string]interface{})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom)
|
2020-09-24 16:51:13 +00:00
|
|
|
errors := errorListDTO{
|
2020-09-17 15:51:19 +00:00
|
|
|
"policy": map[string]string{},
|
|
|
|
"homescreen": map[string]string{},
|
|
|
|
}
|
|
|
|
for _, id := range req.ApplyTo {
|
|
|
|
status, err := app.jf.setPolicy(id, policy)
|
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
|
|
|
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
|
|
|
|
}
|
|
|
|
if req.Homescreen {
|
|
|
|
status, err = app.jf.setConfiguration(id, configuration)
|
|
|
|
errorString := ""
|
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
|
|
|
errorString += fmt.Sprintf("Configuration %d: %s ", status, err)
|
|
|
|
} else {
|
|
|
|
status, err = app.jf.setDisplayPreferences(id, displayprefs)
|
|
|
|
if !(status == 200 || status == 204) || err != nil {
|
|
|
|
errorString += fmt.Sprintf("Displayprefs %d: %s ", status, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if errorString != "" {
|
|
|
|
errors["homescreen"][id] = errorString
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
code := 200
|
|
|
|
if len(errors["policy"]) == len(req.ApplyTo) || len(errors["homescreen"]) == len(req.ApplyTo) {
|
|
|
|
code = 500
|
|
|
|
}
|
|
|
|
gc.JSON(code, errors)
|
|
|
|
}
|
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Get jfa-go configuration.
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} configDTO "Uses the same format as config-base.json"
|
|
|
|
// @Router /config [get]
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) GetConfig(gc *gin.Context) {
|
|
|
|
app.info.Println("Config requested")
|
2020-07-31 15:09:30 +00:00
|
|
|
resp := map[string]interface{}{}
|
2020-08-16 12:36:54 +00:00
|
|
|
for section, settings := range app.configBase {
|
2020-07-31 15:09:30 +00:00
|
|
|
if section == "order" {
|
|
|
|
resp[section] = settings.([]interface{})
|
|
|
|
} else {
|
|
|
|
resp[section] = make(map[string]interface{})
|
|
|
|
for key, values := range settings.(map[string]interface{}) {
|
|
|
|
if key == "order" {
|
|
|
|
resp[section].(map[string]interface{})[key] = values.([]interface{})
|
|
|
|
} else {
|
|
|
|
resp[section].(map[string]interface{})[key] = values.(map[string]interface{})
|
|
|
|
if key != "meta" {
|
|
|
|
dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string)
|
2020-08-16 12:36:54 +00:00
|
|
|
configKey := app.config.Section(section).Key(key)
|
2020-07-31 15:09:30 +00:00
|
|
|
if dataType == "number" {
|
|
|
|
if val, err := configKey.Int(); err == nil {
|
|
|
|
resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val
|
|
|
|
}
|
|
|
|
} else if dataType == "bool" {
|
|
|
|
resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.MustBool(false)
|
|
|
|
} else {
|
|
|
|
resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.String()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
gc.JSON(200, resp)
|
|
|
|
}
|
2020-07-31 21:07:09 +00:00
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Modify app config.
|
|
|
|
// @Produce json
|
|
|
|
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings."
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Router /config [post]
|
|
|
|
|
|
|
|
type configDTO map[string]interface{}
|
|
|
|
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) ModifyConfig(gc *gin.Context) {
|
|
|
|
app.info.Println("Config modification requested")
|
2020-09-24 16:51:13 +00:00
|
|
|
var req configDTO
|
2020-07-31 21:07:09 +00:00
|
|
|
gc.BindJSON(&req)
|
2020-08-16 12:36:54 +00:00
|
|
|
tempConfig, _ := ini.Load(app.config_path)
|
2020-07-31 21:07:09 +00:00
|
|
|
for section, settings := range req {
|
2020-09-23 16:20:48 +00:00
|
|
|
if section != "restart-program" {
|
|
|
|
_, err := tempConfig.GetSection(section)
|
|
|
|
if err != nil {
|
|
|
|
tempConfig.NewSection(section)
|
|
|
|
}
|
2020-07-31 21:07:09 +00:00
|
|
|
for setting, value := range settings.(map[string]interface{}) {
|
|
|
|
tempConfig.Section(section).Key(setting).SetValue(value.(string))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
tempConfig.SaveTo(app.config_path)
|
|
|
|
app.debug.Println("Config saved")
|
2020-07-31 21:07:09 +00:00
|
|
|
gc.JSON(200, map[string]bool{"success": true})
|
2020-09-22 19:46:48 +00:00
|
|
|
if req["restart-program"] != nil && req["restart-program"].(bool) {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.info.Println("Restarting...")
|
|
|
|
err := app.Restart()
|
2020-08-01 23:05:35 +00:00
|
|
|
if err != nil {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.err.Printf("Couldn't restart, try restarting manually. (%s)", err)
|
2020-08-01 23:05:35 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
app.loadConfig()
|
2020-08-02 23:12:45 +00:00
|
|
|
// Reinitialize password validator on config change, as opposed to every applicable request like in python.
|
|
|
|
if _, ok := req["password_validation"]; ok {
|
2020-08-16 12:36:54 +00:00
|
|
|
app.debug.Println("Reinitializing validator")
|
2020-08-02 23:12:45 +00:00
|
|
|
validatorConf := ValidatorConf{
|
2020-08-16 12:36:54 +00:00
|
|
|
"characters": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
|
|
|
"uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0),
|
|
|
|
"lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0),
|
|
|
|
"numbers": app.config.Section("password_validation").Key("number").MustInt(0),
|
|
|
|
"special characters": app.config.Section("password_validation").Key("special").MustInt(0),
|
2020-08-02 23:12:45 +00:00
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
2020-08-02 23:12:45 +00:00
|
|
|
for key := range validatorConf {
|
|
|
|
validatorConf[key] = 0
|
|
|
|
}
|
|
|
|
}
|
2020-08-16 12:36:54 +00:00
|
|
|
app.validator.init(validatorConf)
|
2020-08-02 23:12:45 +00:00
|
|
|
}
|
2020-07-31 21:07:09 +00:00
|
|
|
}
|
2020-08-01 23:05:35 +00:00
|
|
|
|
2020-09-24 16:51:13 +00:00
|
|
|
// @Summary Logout by deleting refresh token from cookies.
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} boolResponse
|
|
|
|
// @Failure 500 {object} stringResponse
|
|
|
|
// @Router /logout [post]
|
2020-08-19 21:30:54 +00:00
|
|
|
func (app *appContext) Logout(gc *gin.Context) {
|
2020-08-20 19:20:31 +00:00
|
|
|
cookie, err := gc.Cookie("refresh")
|
|
|
|
if err != nil {
|
|
|
|
app.debug.Printf("Couldn't get cookies: %s", err)
|
|
|
|
respond(500, "Couldn't fetch cookies", gc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
app.invalidTokens = append(app.invalidTokens, cookie)
|
|
|
|
gc.SetCookie("refresh", "invalid", -1, "/", gc.Request.URL.Hostname(), true, true)
|
2020-09-24 16:51:13 +00:00
|
|
|
respondBool(200, true, gc)
|
2020-08-19 21:30:54 +00:00
|
|
|
}
|
|
|
|
|
2020-08-01 23:05:35 +00:00
|
|
|
// func Restart() error {
|
|
|
|
// defer func() {
|
|
|
|
// if r := recover(); r != nil {
|
|
|
|
// os.Exit(0)
|
|
|
|
// }
|
|
|
|
// }()
|
|
|
|
// cwd, err := os.Getwd()
|
|
|
|
// if err != nil {
|
|
|
|
// return err
|
|
|
|
// }
|
|
|
|
// args := os.Args
|
|
|
|
// // for _, key := range args {
|
|
|
|
// // fmt.Println(key)
|
|
|
|
// // }
|
|
|
|
// cmd := exec.Command(args[0], args[1:]...)
|
|
|
|
// cmd.Stdout = os.Stdout
|
|
|
|
// cmd.Stderr = os.Stderr
|
|
|
|
// cmd.Dir = cwd
|
|
|
|
// err = cmd.Start()
|
|
|
|
// if err != nil {
|
|
|
|
// return err
|
|
|
|
// }
|
|
|
|
// // cmd.Process.Release()
|
|
|
|
// panic(fmt.Errorf("restarting"))
|
|
|
|
// }
|
|
|
|
|
2020-09-08 22:08:50 +00:00
|
|
|
// func (app *appContext) Restart() error {
|
|
|
|
// defer func() {
|
|
|
|
// if r := recover(); r != nil {
|
|
|
|
// signal.Notify(app.quit, os.Interrupt)
|
|
|
|
// <-app.quit
|
|
|
|
// }
|
|
|
|
// }()
|
|
|
|
// args := os.Args
|
|
|
|
// // After a single restart, args[0] gets messed up and isnt the real executable.
|
|
|
|
// // JFA_DEEP tells the new process its a child, and JFA_EXEC is the real executable
|
|
|
|
// if os.Getenv("JFA_DEEP") == "" {
|
|
|
|
// os.Setenv("JFA_DEEP", "1")
|
|
|
|
// os.Setenv("JFA_EXEC", args[0])
|
|
|
|
// }
|
|
|
|
// env := os.Environ()
|
|
|
|
// err := syscall.Exec(os.Getenv("JFA_EXEC"), []string{""}, env)
|
|
|
|
// if err != nil {
|
|
|
|
// return err
|
|
|
|
// }
|
|
|
|
// panic(fmt.Errorf("restarting"))
|
|
|
|
// }
|
|
|
|
|
|
|
|
// no need to syscall.exec anymore!
|
2020-08-16 12:36:54 +00:00
|
|
|
func (app *appContext) Restart() error {
|
2020-09-08 22:08:50 +00:00
|
|
|
RESTART <- true
|
|
|
|
return nil
|
2020-08-01 23:05:35 +00:00
|
|
|
}
|