mirror of
https://github.com/hrfee/jfa-go.git
synced 2025-01-01 05:50:12 +00:00
Compare commits
5 Commits
87ef71b415
...
0e21942cd6
Author | SHA1 | Date | |
---|---|---|---|
0e21942cd6 | |||
b2b5083102 | |||
c0f316d049 | |||
2c6d08319b | |||
5d8f139356 |
3
Makefile
3
Makefile
@ -31,11 +31,13 @@ DEBUG ?= off
|
||||
ifeq ($(DEBUG), on)
|
||||
LDFLAGS := -s -w $(LDFLAGS)
|
||||
SOURCEMAP := --sourcemap
|
||||
TYPECHECK := tsc -noEmit --project ts/tsconfig.json
|
||||
# jank
|
||||
COPYTS := rm -r $(DATA)/web/js/ts; cp -r ts $(DATA)/web/js
|
||||
else
|
||||
SOURCEMAP :=
|
||||
COPYTS :=
|
||||
TYPECHECK :=
|
||||
endif
|
||||
|
||||
npm:
|
||||
@ -59,6 +61,7 @@ email:
|
||||
python3 scripts/compile_mjml.py -o $(DATA)/
|
||||
|
||||
typescript:
|
||||
$(TYPECHECK)
|
||||
$(info compiling typescript)
|
||||
-mkdir -p $(DATA)/web/js
|
||||
-$(ESBUILD) --bundle ts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js --minify
|
||||
|
405
api.go
405
api.go
@ -160,20 +160,25 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
notify := inv.Notify
|
||||
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", code)
|
||||
var wait sync.WaitGroup
|
||||
for address, settings := range notify {
|
||||
if settings["notify-expiry"] {
|
||||
go func() {
|
||||
msg, err := app.email.constructExpiry(code, inv, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
|
||||
} else if err := app.email.send(msg, address); err != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to %s", address)
|
||||
}
|
||||
}()
|
||||
if !settings["notify-expiry"] {
|
||||
continue
|
||||
}
|
||||
wait.Add(1)
|
||||
go func(addr string) {
|
||||
defer wait.Done()
|
||||
msg, err := app.email.constructExpiry(code, inv, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
|
||||
} else if err := app.email.send(msg, addr); err != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to %s", addr)
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
wait.Wait()
|
||||
}
|
||||
changed = true
|
||||
match = false
|
||||
@ -313,6 +318,7 @@ func (app *appContext) NewUserAdmin(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) {
|
||||
existingUser, _, _ := app.jf.UserByName(req.Username, false)
|
||||
if existingUser.Name != "" {
|
||||
@ -460,38 +466,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
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
|
||||
// @Produce json
|
||||
// @Param newUserDTO body newUserDTO true "New user request object"
|
||||
@ -535,44 +509,6 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
||||
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.
|
||||
// @Produce json
|
||||
// @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)
|
||||
}
|
||||
|
||||
// @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.
|
||||
// @Produce json
|
||||
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
|
||||
@ -775,6 +781,99 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
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
|
||||
// @Produce json
|
||||
// @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object"
|
||||
@ -913,99 +1012,6 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
|
||||
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.
|
||||
// @Produce json
|
||||
// @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
|
||||
// @Router /config/emails [get]
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetEmails(gc *gin.Context) {
|
||||
func (app *appContext) GetCustomEmails(gc *gin.Context) {
|
||||
lang := gc.Query("lang")
|
||||
if _, ok := app.storage.lang.Email[lang]; !ok {
|
||||
lang = app.storage.lang.chosenEmailLang
|
||||
@ -1458,9 +1464,10 @@ func (app *appContext) GetEmails(gc *gin.Context) {
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param id path string true "ID of email"
|
||||
// @Router /config/emails/{id} [post]
|
||||
// @tags Configuration
|
||||
func (app *appContext) SetEmail(gc *gin.Context) {
|
||||
func (app *appContext) SetCustomEmail(gc *gin.Context) {
|
||||
var req customEmail
|
||||
gc.BindJSON(&req)
|
||||
id := gc.Param("id")
|
||||
@ -1515,9 +1522,11 @@ func (app *appContext) SetEmail(gc *gin.Context) {
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {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]
|
||||
// @tags Configuration
|
||||
func (app *appContext) SetEmailState(gc *gin.Context) {
|
||||
func (app *appContext) SetCustomEmailState(gc *gin.Context) {
|
||||
id := gc.Param("id")
|
||||
s := gc.Param("state")
|
||||
enabled := false
|
||||
@ -1563,9 +1572,10 @@ func (app *appContext) SetEmailState(gc *gin.Context) {
|
||||
// @Success 200 {object} customEmailDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param id path string true "ID of email"
|
||||
// @Router /config/emails/{id} [get]
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetEmail(gc *gin.Context) {
|
||||
func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||
lang := app.storage.lang.chosenEmailLang
|
||||
id := gc.Param("id")
|
||||
var content string
|
||||
@ -1702,6 +1712,9 @@ func (app *appContext) GetEmail(gc *gin.Context) {
|
||||
}
|
||||
writeVars = func(variables []string) { app.storage.customEmails.UserExpired.Variables = variables }
|
||||
values = app.email.userExpiredValues(app, false)
|
||||
// Just send the email html
|
||||
case "Announcement":
|
||||
content = ""
|
||||
default:
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
@ -1737,7 +1750,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
email, err := app.email.constructTemplate("", "<div id=\"preview-content\"></div>", app)
|
||||
email, err := app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
|
||||
if err != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
@ -1775,7 +1788,12 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if PLATFORM == "windows" {
|
||||
respondBool(500, true, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
app.HardRestart()
|
||||
}
|
||||
|
||||
// @Summary Logout by deleting refresh token from cookies.
|
||||
@ -1800,6 +1818,7 @@ func (app *appContext) Logout(gc *gin.Context) {
|
||||
// @Produce json
|
||||
// @Success 200 {object} langDTO
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Param page path string true "admin/form/setup/email/pwr"
|
||||
// @Router /lang/{page} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) GetLanguages(gc *gin.Context) {
|
||||
@ -1834,17 +1853,14 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Summary Restarts the program. No response means success.
|
||||
// @Router /restart [post]
|
||||
// @Summary Serves a translations for pages "admin" or "form".
|
||||
// @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
|
||||
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) {
|
||||
page := gc.Param("page")
|
||||
lang := strings.Replace(gc.Param("file"), ".json", "", 1)
|
||||
@ -1858,6 +1874,17 @@ func (app *appContext) ServeLang(gc *gin.Context) {
|
||||
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!
|
||||
func (app *appContext) Restart() error {
|
||||
RESTART <- true
|
||||
|
@ -156,18 +156,24 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-announce" class="modal">
|
||||
<form class="modal-content card" id="form-announce" href="">
|
||||
<form class="modal-content wide card" id="form-announce" href="">
|
||||
<span class="heading"><span id="header-announce"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-half">
|
||||
<label class="label supra" for="announce-subject"> {{ .strings.subject }}</label>
|
||||
<input type="text" id="announce-subject" class="input ~neutral !normal mb-1 mt-half">
|
||||
<label class="label supra" for="textarea-announce">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-announce" class="textarea full-width ~neutral !normal mt-half monospace"></textarea>
|
||||
<p class="support mt-half mb-1">{{ .strings.markdownSupported }}</p>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
<div class="row">
|
||||
<div class="col flex-col content mt-half">
|
||||
<label class="label supra" for="announce-subject"> {{ .strings.subject }}</label>
|
||||
<input type="text" id="announce-subject" class="input ~neutral !normal mb-1 mt-half">
|
||||
<label class="label supra" for="textarea-announce">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-announce" class="textarea full-width ~neutral !normal mt-half monospace"></textarea>
|
||||
<p class="support mt-half mb-1">{{ .strings.markdownSupported }}</p>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col card ~neutral !low">
|
||||
<span class="subheading supra">{{ .strings.preview }}</span>
|
||||
<div class="mt-half" id="announce-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -103,6 +103,7 @@
|
||||
"sentAnnouncement": "Announcement sent.",
|
||||
"setOmbiDefaults": "Stored ombi defaults.",
|
||||
"updateApplied": "Update applied, please restart.",
|
||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||
|
13
package-lock.json
generated
13
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ts-stack/markdown": "^1.3.0",
|
||||
"@types/node": "^15.0.1",
|
||||
"a17t": "^0.4.0",
|
||||
"esbuild": "^0.8.57",
|
||||
"lodash": "^4.17.19",
|
||||
@ -35,9 +36,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "14.14.16",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-14.14.16.tgz?cache=0&sync_timestamp=1608756036972&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-14.14.16.tgz",
|
||||
"integrity": "sha1-PMNR+NSBAd6t/tTJ5PEWBI1De0s="
|
||||
"version": "15.0.1",
|
||||
"resolved": "https://registry.nlark.com/@types/node/download/@types/node-15.0.1.tgz?cache=0&sync_timestamp=1619534647758&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-15.0.1.tgz",
|
||||
"integrity": "sha1-7zTeoIgQKNETmL5b9OhWdD49w1o="
|
||||
},
|
||||
"node_modules/a17t": {
|
||||
"version": "0.4.0",
|
||||
@ -1835,9 +1836,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.16",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-14.14.16.tgz?cache=0&sync_timestamp=1608756036972&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-14.14.16.tgz",
|
||||
"integrity": "sha1-PMNR+NSBAd6t/tTJ5PEWBI1De0s="
|
||||
"version": "15.0.1",
|
||||
"resolved": "https://registry.nlark.com/@types/node/download/@types/node-15.0.1.tgz?cache=0&sync_timestamp=1619534647758&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-15.0.1.tgz",
|
||||
"integrity": "sha1-7zTeoIgQKNETmL5b9OhWdD49w1o="
|
||||
},
|
||||
"a17t": {
|
||||
"version": "0.4.0",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"homepage": "https://github.com/hrfee/jfa-go#readme",
|
||||
"dependencies": {
|
||||
"@ts-stack/markdown": "^1.3.0",
|
||||
"@types/node": "^15.0.1",
|
||||
"a17t": "^0.4.0",
|
||||
"esbuild": "^0.8.57",
|
||||
"lodash": "^4.17.19",
|
||||
|
32
restart.go
Normal file
32
restart.go
Normal file
@ -0,0 +1,32 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (app *appContext) HardRestart() 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("r"))
|
||||
}
|
7
restart_windows.go
Normal file
7
restart_windows.go
Normal file
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (app *appContext) HardRestart() error {
|
||||
return fmt.Errorf("hard restarts not available on windows")
|
||||
}
|
@ -148,10 +148,10 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.POST(p+"/users/announce", app.Announce)
|
||||
api.GET(p+"/config/update", app.CheckUpdate)
|
||||
api.POST(p+"/config/update", app.ApplyUpdate)
|
||||
api.GET(p+"/config/emails", app.GetEmails)
|
||||
api.GET(p+"/config/emails/:id", app.GetEmail)
|
||||
api.POST(p+"/config/emails/:id", app.SetEmail)
|
||||
api.POST(p+"/config/emails/:id/state/:state", app.SetEmailState)
|
||||
api.GET(p+"/config/emails", app.GetCustomEmails)
|
||||
api.GET(p+"/config/emails/:id", app.GetCustomEmailTemplate)
|
||||
api.POST(p+"/config/emails/:id", app.SetCustomEmail)
|
||||
api.POST(p+"/config/emails/:id/state/:state", app.SetCustomEmailState)
|
||||
api.GET(p+"/config", app.GetConfig)
|
||||
api.POST(p+"/config", app.ModifyConfig)
|
||||
api.POST(p+"/restart", app.restart)
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { _get, _post, _delete, toggleLoader, toDateString } from "../modules/common.js";
|
||||
import { templateEmail } from "../modules/settings.js";
|
||||
import { Marked } from "@ts-stack/markdown";
|
||||
import { stripMarkdown } from "../modules/stripmd.js";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@ -193,6 +196,9 @@ export class accountsList {
|
||||
|
||||
private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement;
|
||||
private _announceButton = document.getElementById("accounts-announce") as HTMLSpanElement;
|
||||
private _announcePreview: HTMLElement;
|
||||
private _previewLoaded = false;
|
||||
private _announceTextarea = document.getElementById("textarea-announce") as HTMLTextAreaElement;
|
||||
private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement;
|
||||
private _disableEnable = document.getElementById("accounts-disable-enable") as HTMLSpanElement;
|
||||
private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement;
|
||||
@ -432,7 +438,16 @@ export class accountsList {
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
loadPreview = () => {
|
||||
let content = this._announceTextarea.value;
|
||||
if (!this._previewLoaded) {
|
||||
content = stripMarkdown(content);
|
||||
this._announcePreview.textContent = content;
|
||||
} else {
|
||||
content = Marked.parse(content);
|
||||
this._announcePreview.innerHTML = content;
|
||||
}
|
||||
}
|
||||
announce = () => {
|
||||
const modalHeader = document.getElementById("header-announce");
|
||||
modalHeader.textContent = window.lang.quantity("announceTo", this._collectUsers().length);
|
||||
@ -440,16 +455,16 @@ export class accountsList {
|
||||
let list = this._collectUsers();
|
||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
const subject = document.getElementById("announce-subject") as HTMLInputElement;
|
||||
const message = document.getElementById("textarea-announce") as HTMLTextAreaElement;
|
||||
|
||||
subject.value = "";
|
||||
message.value = "";
|
||||
this._announceTextarea.value = "";
|
||||
form.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
toggleLoader(button);
|
||||
let send = {
|
||||
"users": list,
|
||||
"subject": subject.value,
|
||||
"message": message.value
|
||||
"message": this._announceTextarea.value
|
||||
}
|
||||
_post("/users/announce", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
@ -463,7 +478,29 @@ export class accountsList {
|
||||
}
|
||||
});
|
||||
};
|
||||
window.modals.announce.show();
|
||||
_get("/config/emails/Announcement", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
const preview = document.getElementById("announce-preview") as HTMLDivElement;
|
||||
if (req.status != 200) {
|
||||
preview.innerHTML = `<pre class="preview-content" class="monospace"></pre>`;
|
||||
window.modals.announce.show();
|
||||
this._previewLoaded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let templ = req.response as templateEmail;
|
||||
if (!templ.html) {
|
||||
preview.innerHTML = `<pre class="preview-content" class="monospace"></pre>`;
|
||||
this._previewLoaded = false;
|
||||
} else {
|
||||
preview.innerHTML = templ.html;
|
||||
this._previewLoaded = true;
|
||||
}
|
||||
this._announcePreview = preview.getElementsByClassName("preview-content")[0] as HTMLElement;
|
||||
this.loadPreview();
|
||||
window.modals.announce.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableDisableUsers = () => {
|
||||
@ -750,6 +787,8 @@ export class accountsList {
|
||||
}
|
||||
this._checkCheckCount();
|
||||
};
|
||||
|
||||
this._announceTextarea.onkeyup = this.loadPreview;
|
||||
}
|
||||
|
||||
reload = () => _get("/users", null, (req: XMLHttpRequest) => {
|
||||
@ -769,7 +808,7 @@ export class accountsList {
|
||||
this._users[id].remove();
|
||||
delete this._users[id];
|
||||
}
|
||||
this._checkCheckCount;
|
||||
this._checkCheckCount();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ class DOMInvite implements Invite {
|
||||
this._createdUnix = unix;
|
||||
const el = this._middle.querySelector("strong.inv-created");
|
||||
if (unix == 0) {
|
||||
el.textContent = "n/a";
|
||||
el.textContent = window.lang.strings("unknown");
|
||||
} else {
|
||||
el.textContent = toDateString(new Date(unix*1000));
|
||||
}
|
||||
@ -479,7 +479,7 @@ export class inviteList implements inviteList {
|
||||
}
|
||||
|
||||
|
||||
function parseInvite(invite: { [f: string]: string | number | string[][] | boolean }): Invite {
|
||||
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
|
||||
let parsed: Invite = {};
|
||||
parsed.code = invite["code"] as string;
|
||||
parsed.email = invite["email"] as string || "";
|
||||
@ -509,8 +509,8 @@ function parseInvite(invite: { [f: string]: string | number | string[][] | boole
|
||||
parsed.userExpiry = invite["user-expiry"] as boolean;
|
||||
parsed.userExpiryTime = userExpiryTime.slice(0, -1);
|
||||
parsed.remainingUses = invite["no-limit"] ? "∞" : String(invite["remaining-uses"])
|
||||
parsed.usedBy = invite["used-by"] as string[][] || [];
|
||||
parsed.created = invite["created"] as string || window.lang.strings("unknown");
|
||||
parsed.usedBy = invite["used-by"] as { [name: string]: number } || {} ;
|
||||
parsed.created = invite["created"] as number || 0;
|
||||
parsed.profile = invite["profile"] as string || "";
|
||||
parsed.notifyExpiry = invite["notify-expiry"] as boolean || false;
|
||||
parsed.notifyCreation = invite["notify-creation"] as boolean || false;
|
||||
|
@ -766,7 +766,7 @@ class ombiDefaults {
|
||||
}
|
||||
}
|
||||
|
||||
interface templateEmail {
|
||||
export interface templateEmail {
|
||||
content: string;
|
||||
variables: string[];
|
||||
conditionals: string[];
|
||||
@ -829,11 +829,11 @@ class EmailEditor {
|
||||
this._templ = req.response as templateEmail;
|
||||
this._textArea.value = this._templ.content;
|
||||
if (this._templ.html == "") {
|
||||
this._preview.innerHTML = `<pre id="preview-content" class="monospace"></pre>`;
|
||||
this._preview.innerHTML = `<pre class="preview-content" class="monospace"></pre>`;
|
||||
} else {
|
||||
this._preview.innerHTML = this._templ.html;
|
||||
}
|
||||
this._previewContent = document.getElementById("preview-content");
|
||||
this._previewContent = this._preview.getElementsByClassName("preview-content")[0] as HTMLElement;
|
||||
this.loadPreview();
|
||||
this._content = this._templ.content;
|
||||
const colors = ["info", "urge", "positive", "neutral"];
|
||||
|
@ -107,13 +107,20 @@ export class Updater implements updater {
|
||||
_post("/config/update", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(update);
|
||||
if (req.status != 200) {
|
||||
const success = req.response["success"] as Boolean;
|
||||
if (req.status == 500 && success) {
|
||||
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
|
||||
} else if (req.status != 200) {
|
||||
window.notifications.customError("applyUpdateError", window.lang.notif("errorApplyUpdate"));
|
||||
} else {
|
||||
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateApplied"));
|
||||
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
|
||||
}
|
||||
window.modals.updateInfo.close();
|
||||
}
|
||||
}, true, (req: XMLHttpRequest) => {
|
||||
if (req.status == 0) {
|
||||
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
|
||||
}
|
||||
});
|
||||
};
|
||||
this.checkForUpdates(() => {
|
||||
|
@ -3,7 +3,7 @@
|
||||
"outDir": "../js",
|
||||
"target": "es6",
|
||||
"lib": ["dom", "es2017"],
|
||||
"typeRoots": ["./typings", "./node_modules/@types"],
|
||||
"typeRoots": ["./typings", "../node_modules/@types"],
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
|
11
updater.go
11
updater.go
@ -440,7 +440,16 @@ func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int
|
||||
return
|
||||
}
|
||||
applyUpdate = func() error {
|
||||
return os.Rename(path+"_", path)
|
||||
oldName := path + "-" + version + "-" + commit
|
||||
err := os.Rename(path, oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Rename(path+"_", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Remove(oldName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
6
views.go
6
views.go
@ -53,13 +53,15 @@ func (app *appContext) pushResources(gc *gin.Context, admin bool) {
|
||||
gc.Header("Link", cssHeader)
|
||||
}
|
||||
|
||||
type Page int
|
||||
|
||||
const (
|
||||
AdminPage = iota + 1
|
||||
AdminPage Page = iota + 1
|
||||
FormPage
|
||||
PWRPage
|
||||
)
|
||||
|
||||
func (app *appContext) getLang(gc *gin.Context, page int, chosen string) string {
|
||||
func (app *appContext) getLang(gc *gin.Context, page Page, chosen string) string {
|
||||
lang := gc.Query("lang")
|
||||
cookie, err := gc.Cookie("lang")
|
||||
if lang != "" {
|
||||
|
Loading…
Reference in New Issue
Block a user