1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2025-01-06 08:20:11 +00:00

Compare commits

..

No commits in common. "0e21942cd6616c239e3942b051c3d0168581e0ff" and "87ef71b415788498096198dcfab678dd853a97d9" have entirely different histories.

16 changed files with 229 additions and 364 deletions

View File

@ -31,13 +31,11 @@ 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:
@ -61,7 +59,6 @@ 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
View File

@ -160,25 +160,20 @@ 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"] {
continue
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)
}
}()
}
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
@ -318,7 +313,6 @@ 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 != "" {
@ -466,6 +460,38 @@ 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"
@ -509,6 +535,44 @@ 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"
@ -641,76 +705,6 @@ 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"
@ -781,99 +775,6 @@ 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"
@ -1012,6 +913,99 @@ 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"
@ -1439,7 +1433,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
// @Success 200 {object} emailListDTO
// @Router /config/emails [get]
// @tags Configuration
func (app *appContext) GetCustomEmails(gc *gin.Context) {
func (app *appContext) GetEmails(gc *gin.Context) {
lang := gc.Query("lang")
if _, ok := app.storage.lang.Email[lang]; !ok {
lang = app.storage.lang.chosenEmailLang
@ -1464,10 +1458,9 @@ func (app *appContext) GetCustomEmails(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) SetCustomEmail(gc *gin.Context) {
func (app *appContext) SetEmail(gc *gin.Context) {
var req customEmail
gc.BindJSON(&req)
id := gc.Param("id")
@ -1522,11 +1515,9 @@ func (app *appContext) SetCustomEmail(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) SetCustomEmailState(gc *gin.Context) {
func (app *appContext) SetEmailState(gc *gin.Context) {
id := gc.Param("id")
s := gc.Param("state")
enabled := false
@ -1572,10 +1563,9 @@ func (app *appContext) SetCustomEmailState(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) GetCustomEmailTemplate(gc *gin.Context) {
func (app *appContext) GetEmail(gc *gin.Context) {
lang := app.storage.lang.chosenEmailLang
id := gc.Param("id")
var content string
@ -1712,9 +1702,6 @@ func (app *appContext) GetCustomEmailTemplate(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
@ -1750,7 +1737,7 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
respondBool(500, false, gc)
return
}
email, err := app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
email, err := app.email.constructTemplate("", "<div id=\"preview-content\"></div>", app)
if err != nil {
respondBool(500, false, gc)
return
@ -1788,12 +1775,7 @@ 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.
@ -1818,7 +1800,6 @@ 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) {
@ -1853,14 +1834,17 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
gc.JSON(200, resp)
}
// @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]
// @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)
}
}
func (app *appContext) ServeLang(gc *gin.Context) {
page := gc.Param("page")
lang := strings.Replace(gc.Param("file"), ".json", "", 1)
@ -1874,17 +1858,6 @@ 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

View File

@ -156,24 +156,18 @@
</form>
</div>
<div id="modal-announce" class="modal">
<form class="modal-content wide card" id="form-announce" href="">
<form class="modal-content card" id="form-announce" href="">
<span class="heading"><span id="header-announce"></span> <span class="modal-close">&times;</span></span>
<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 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>
</form>
</div>

View File

@ -103,7 +103,6 @@
"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
View File

@ -9,7 +9,6 @@
"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",
@ -36,9 +35,9 @@
}
},
"node_modules/@types/node": {
"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="
"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="
},
"node_modules/a17t": {
"version": "0.4.0",
@ -1836,9 +1835,9 @@
}
},
"@types/node": {
"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="
"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="
},
"a17t": {
"version": "0.4.0",

View File

@ -18,7 +18,6 @@
"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",

View File

@ -1,32 +0,0 @@
// +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"))
}

View File

@ -1,7 +0,0 @@
package main
import "fmt"
func (app *appContext) HardRestart() error {
return fmt.Errorf("hard restarts not available on windows")
}

View File

@ -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.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/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", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)

View File

@ -1,7 +1,4 @@
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;
@ -196,9 +193,6 @@ 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;
@ -438,16 +432,7 @@ 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);
@ -455,16 +440,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 = "";
this._announceTextarea.value = "";
message.value = "";
form.onsubmit = (event: Event) => {
event.preventDefault();
toggleLoader(button);
let send = {
"users": list,
"subject": subject.value,
"message": this._announceTextarea.value
"message": message.value
}
_post("/users/announce", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
@ -478,29 +463,7 @@ export class accountsList {
}
});
};
_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();
}
});
window.modals.announce.show();
}
enableDisableUsers = () => {
@ -787,8 +750,6 @@ export class accountsList {
}
this._checkCheckCount();
};
this._announceTextarea.onkeyup = this.loadPreview;
}
reload = () => _get("/users", null, (req: XMLHttpRequest) => {
@ -808,7 +769,7 @@ export class accountsList {
this._users[id].remove();
delete this._users[id];
}
this._checkCheckCount();
this._checkCheckCount;
}
})
}

View File

@ -157,7 +157,7 @@ class DOMInvite implements Invite {
this._createdUnix = unix;
const el = this._middle.querySelector("strong.inv-created");
if (unix == 0) {
el.textContent = window.lang.strings("unknown");
el.textContent = "n/a";
} else {
el.textContent = toDateString(new Date(unix*1000));
}
@ -479,7 +479,7 @@ export class inviteList implements inviteList {
}
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
function parseInvite(invite: { [f: string]: string | number | string[][] | 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 | { [name: string]:
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 { [name: string]: number } || {} ;
parsed.created = invite["created"] as number || 0;
parsed.usedBy = invite["used-by"] as string[][] || [];
parsed.created = invite["created"] as string || window.lang.strings("unknown");
parsed.profile = invite["profile"] as string || "";
parsed.notifyExpiry = invite["notify-expiry"] as boolean || false;
parsed.notifyCreation = invite["notify-creation"] as boolean || false;

View File

@ -766,7 +766,7 @@ class ombiDefaults {
}
}
export interface templateEmail {
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 class="preview-content" class="monospace"></pre>`;
this._preview.innerHTML = `<pre id="preview-content" class="monospace"></pre>`;
} else {
this._preview.innerHTML = this._templ.html;
}
this._previewContent = this._preview.getElementsByClassName("preview-content")[0] as HTMLElement;
this._previewContent = document.getElementById("preview-content");
this.loadPreview();
this._content = this._templ.content;
const colors = ["info", "urge", "positive", "neutral"];

View File

@ -107,20 +107,13 @@ export class Updater implements updater {
_post("/config/update", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(update);
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) {
if (req.status != 200) {
window.notifications.customError("applyUpdateError", window.lang.notif("errorApplyUpdate"));
} else {
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateApplied"));
}
window.modals.updateInfo.close();
}
}, true, (req: XMLHttpRequest) => {
if (req.status == 0) {
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
}
});
};
this.checkForUpdates(() => {

View File

@ -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
}

View File

@ -440,16 +440,7 @@ func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int
return
}
applyUpdate = func() error {
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 os.Rename(path+"_", path)
}
return
}

View File

@ -53,15 +53,13 @@ func (app *appContext) pushResources(gc *gin.Context, admin bool) {
gc.Header("Link", cssHeader)
}
type Page int
const (
AdminPage Page = iota + 1
AdminPage = iota + 1
FormPage
PWRPage
)
func (app *appContext) getLang(gc *gin.Context, page Page, chosen string) string {
func (app *appContext) getLang(gc *gin.Context, page int, chosen string) string {
lang := gc.Query("lang")
cookie, err := gc.Cookie("lang")
if lang != "" {