mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-29 12:30:11 +00:00
refactor, should have been in main
This commit is contained in:
parent
d9354c7d6b
commit
b3d7bc7704
@ -39,9 +39,8 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
|||||||
|
|
||||||
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/).
|
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/).
|
||||||
|
|
||||||
For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.pw/view/hrfee/jfa-go)), and extract `jfa-go` and `data` to the same directory.
|
For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.pw/view/hrfee/jfa-go)), and extract the `jfa-go` executable to somewhere useful.
|
||||||
* For linux users, you can place them inside `/opt/jfa-go` and then run
|
* For \*nix/macOS users, `chmod +x jfa-go` then place it somewhere in your PATH like `/usr/bin`.
|
||||||
`sudo ln -s /opt/jfa-go/jfa-go /usr/bin/jfa-go` to place it in your PATH.
|
|
||||||
|
|
||||||
Run the executable to start.
|
Run the executable to start.
|
||||||
|
|
||||||
|
3
api.go
3
api.go
@ -1330,6 +1330,9 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
|
|||||||
gc.JSON(200, resp)
|
gc.JSON(200, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Restarts the program. No response means success.
|
||||||
|
// @Router /restart [post]
|
||||||
|
// @tags Other
|
||||||
func (app *appContext) restart(gc *gin.Context) {
|
func (app *appContext) restart(gc *gin.Context) {
|
||||||
app.info.Println("Restarting...")
|
app.info.Println("Restarting...")
|
||||||
err := app.Restart()
|
err := app.Restart()
|
||||||
|
262
email.go
262
email.go
@ -153,6 +153,28 @@ func (emailer *Emailer) NewSMTP(server string, port int, username, password stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text string, err error) {
|
||||||
|
var tpl *template.Template
|
||||||
|
for _, key := range []string{"html", "text"} {
|
||||||
|
filesystem, fpath := app.GetPath(section, keyFragment+key)
|
||||||
|
tpl, err = template.ParseFS(filesystem, fpath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var tplData bytes.Buffer
|
||||||
|
err = tpl.Execute(&tplData, data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if key == "html" {
|
||||||
|
html = tplData.String()
|
||||||
|
} else {
|
||||||
|
text = tplData.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext) (*Email, error) {
|
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext) (*Email, error) {
|
||||||
email := &Email{
|
email := &Email{
|
||||||
subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
|
subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
|
||||||
@ -160,30 +182,17 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
|||||||
message := app.config.Section("email").Key("message").String()
|
message := app.config.Section("email").Key("message").String()
|
||||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||||
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
|
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
|
||||||
|
var err error
|
||||||
for _, key := range []string{"html", "text"} {
|
email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", map[string]interface{}{
|
||||||
filesystem, fpath := app.GetPath("email_confirmation", "email_"+key)
|
"helloUser": emailer.lang.Strings.format("helloUser", username),
|
||||||
tpl, err := template.ParseFS(filesystem, fpath)
|
"clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"),
|
||||||
if err != nil {
|
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
|
||||||
return nil, err
|
"urlVal": inviteLink,
|
||||||
}
|
"confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"),
|
||||||
var tplData bytes.Buffer
|
"message": message,
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
})
|
||||||
"helloUser": emailer.lang.Strings.format("helloUser", username),
|
if err != nil {
|
||||||
"clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"),
|
return nil, err
|
||||||
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
|
|
||||||
"urlVal": inviteLink,
|
|
||||||
"confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"),
|
|
||||||
"message": message,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if key == "html" {
|
|
||||||
email.html = tplData.String()
|
|
||||||
} else {
|
|
||||||
email.text = tplData.String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return email, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
@ -197,31 +206,18 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
|
|||||||
message := app.config.Section("email").Key("message").String()
|
message := app.config.Section("email").Key("message").String()
|
||||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||||
inviteLink = fmt.Sprintf("%s/%s", inviteLink, code)
|
inviteLink = fmt.Sprintf("%s/%s", inviteLink, code)
|
||||||
|
var err error
|
||||||
for _, key := range []string{"html", "text"} {
|
email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", map[string]interface{}{
|
||||||
filesystem, fpath := app.GetPath("invite_emails", "email_"+key)
|
"hello": emailer.lang.InviteEmail.get("hello"),
|
||||||
tpl, err := template.ParseFS(filesystem, fpath)
|
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
|
||||||
if err != nil {
|
"toJoin": emailer.lang.InviteEmail.get("toJoin"),
|
||||||
return nil, err
|
"inviteExpiry": emailer.lang.InviteEmail.format("inviteExpiry", d, t, expiresIn),
|
||||||
}
|
"linkButton": emailer.lang.InviteEmail.get("linkButton"),
|
||||||
var tplData bytes.Buffer
|
"invite_link": inviteLink,
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
"message": message,
|
||||||
"hello": emailer.lang.InviteEmail.get("hello"),
|
})
|
||||||
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
|
if err != nil {
|
||||||
"toJoin": emailer.lang.InviteEmail.get("toJoin"),
|
return nil, err
|
||||||
"inviteExpiry": emailer.lang.InviteEmail.format("inviteExpiry", d, t, expiresIn),
|
|
||||||
"linkButton": emailer.lang.InviteEmail.get("linkButton"),
|
|
||||||
"invite_link": inviteLink,
|
|
||||||
"message": message,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if key == "html" {
|
|
||||||
email.html = tplData.String()
|
|
||||||
} else {
|
|
||||||
email.text = tplData.String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return email, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
@ -231,26 +227,14 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
|
|||||||
subject: emailer.lang.InviteExpiry.get("title"),
|
subject: emailer.lang.InviteExpiry.get("title"),
|
||||||
}
|
}
|
||||||
expiry := app.formatDatetime(invite.ValidTill)
|
expiry := app.formatDatetime(invite.ValidTill)
|
||||||
for _, key := range []string{"html", "text"} {
|
var err error
|
||||||
filesystem, fpath := app.GetPath("notifications", "expiry_"+key)
|
email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", map[string]interface{}{
|
||||||
tpl, err := template.ParseFS(filesystem, fpath)
|
"inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
|
||||||
if err != nil {
|
"expiredAt": emailer.lang.InviteExpiry.format("expiredAt", "\""+code+"\"", expiry),
|
||||||
return nil, err
|
"notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"),
|
||||||
}
|
})
|
||||||
var tplData bytes.Buffer
|
if err != nil {
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
return nil, err
|
||||||
"inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
|
|
||||||
"expiredAt": emailer.lang.InviteExpiry.format("expiredAt", "\""+code+"\"", expiry),
|
|
||||||
"notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if key == "html" {
|
|
||||||
email.html = tplData.String()
|
|
||||||
} else {
|
|
||||||
email.text = tplData.String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return email, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
@ -266,31 +250,19 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
|||||||
} else {
|
} else {
|
||||||
tplAddress = address
|
tplAddress = address
|
||||||
}
|
}
|
||||||
for _, key := range []string{"html", "text"} {
|
var err error
|
||||||
filesystem, fpath := app.GetPath("notifications", "created_"+key)
|
email.html, email.text, err = emailer.construct(app, "notifications", "created_", map[string]interface{}{
|
||||||
tpl, err := template.ParseFS(filesystem, fpath)
|
"aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""),
|
||||||
if err != nil {
|
"name": emailer.lang.Strings.get("name"),
|
||||||
return nil, err
|
"address": emailer.lang.Strings.get("emailAddress"),
|
||||||
}
|
"time": emailer.lang.UserCreated.get("time"),
|
||||||
var tplData bytes.Buffer
|
"nameVal": username,
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
"addressVal": tplAddress,
|
||||||
"aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""),
|
"timeVal": created,
|
||||||
"name": emailer.lang.Strings.get("name"),
|
"notificationNotice": emailer.lang.UserCreated.get("notificationNotice"),
|
||||||
"address": emailer.lang.Strings.get("emailAddress"),
|
})
|
||||||
"time": emailer.lang.UserCreated.get("time"),
|
if err != nil {
|
||||||
"nameVal": username,
|
return nil, err
|
||||||
"addressVal": tplAddress,
|
|
||||||
"timeVal": created,
|
|
||||||
"notificationNotice": emailer.lang.UserCreated.get("notificationNotice"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if key == "html" {
|
|
||||||
email.html = tplData.String()
|
|
||||||
} else {
|
|
||||||
email.text = tplData.String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return email, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
@ -301,31 +273,19 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema
|
|||||||
}
|
}
|
||||||
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
||||||
message := app.config.Section("email").Key("message").String()
|
message := app.config.Section("email").Key("message").String()
|
||||||
for _, key := range []string{"html", "text"} {
|
var err error
|
||||||
filesystem, fpath := app.GetPath("password_resets", "email_"+key)
|
email.html, email.text, err = emailer.construct(app, "password_resets", "email_", map[string]interface{}{
|
||||||
tpl, err := template.ParseFS(filesystem, fpath)
|
"helloUser": emailer.lang.Strings.format("helloUser", pwr.Username),
|
||||||
if err != nil {
|
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
|
||||||
return nil, err
|
"ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"),
|
||||||
}
|
"codeExpiry": emailer.lang.PasswordReset.format("codeExpiry", d, t, expiresIn),
|
||||||
var tplData bytes.Buffer
|
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
"pin": emailer.lang.PasswordReset.get("pin"),
|
||||||
"helloUser": emailer.lang.Strings.format("helloUser", pwr.Username),
|
"pinVal": pwr.Pin,
|
||||||
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
|
"message": message,
|
||||||
"ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"),
|
})
|
||||||
"codeExpiry": emailer.lang.PasswordReset.format("codeExpiry", d, t, expiresIn),
|
if err != nil {
|
||||||
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
|
return nil, err
|
||||||
"pin": emailer.lang.PasswordReset.get("pin"),
|
|
||||||
"pinVal": pwr.Pin,
|
|
||||||
"message": message,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if key == "html" {
|
|
||||||
email.html = tplData.String()
|
|
||||||
} else {
|
|
||||||
email.text = tplData.String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return email, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
@ -334,26 +294,14 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email
|
|||||||
email := &Email{
|
email := &Email{
|
||||||
subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
|
subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
|
||||||
}
|
}
|
||||||
for _, key := range []string{"html", "text"} {
|
var err error
|
||||||
filesystem, fpath := app.GetPath("deletion", "email_"+key)
|
email.html, email.text, err = emailer.construct(app, "deletion", "email_", map[string]interface{}{
|
||||||
tpl, err := template.ParseFS(filesystem, fpath)
|
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
|
||||||
if err != nil {
|
"reason": emailer.lang.UserDeleted.get("reason"),
|
||||||
return nil, err
|
"reasonVal": reason,
|
||||||
}
|
})
|
||||||
var tplData bytes.Buffer
|
if err != nil {
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
return nil, err
|
||||||
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
|
|
||||||
"reason": emailer.lang.UserDeleted.get("reason"),
|
|
||||||
"reasonVal": reason,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if key == "html" {
|
|
||||||
email.html = tplData.String()
|
|
||||||
} else {
|
|
||||||
email.text = tplData.String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return email, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
@ -362,30 +310,18 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext) (*Ema
|
|||||||
email := &Email{
|
email := &Email{
|
||||||
subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
|
subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
|
||||||
}
|
}
|
||||||
for _, key := range []string{"html", "text"} {
|
var err error
|
||||||
filesystem, fpath := app.GetPath("welcome_email", "email_"+key)
|
email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", map[string]interface{}{
|
||||||
tpl, err := template.ParseFS(filesystem, fpath)
|
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
|
||||||
if err != nil {
|
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
|
||||||
return nil, err
|
"jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"),
|
||||||
}
|
"jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(),
|
||||||
var tplData bytes.Buffer
|
"username": emailer.lang.Strings.get("username"),
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
"usernameVal": username,
|
||||||
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
|
"message": app.config.Section("email").Key("message").String(),
|
||||||
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
|
})
|
||||||
"jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"),
|
if err != nil {
|
||||||
"jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(),
|
return nil, err
|
||||||
"username": emailer.lang.Strings.get("username"),
|
|
||||||
"usernameVal": username,
|
|
||||||
"message": app.config.Section("email").Key("message").String(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if key == "html" {
|
|
||||||
email.html = tplData.String()
|
|
||||||
} else {
|
|
||||||
email.text = tplData.String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return email, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
|
4
embed.py
4
embed.py
@ -36,7 +36,7 @@ func (l rewriteFS) Open(name string) (fs.File, error) { return l.fs.Ope
|
|||||||
func (l rewriteFS) ReadDir(name string) ([]fs.DirEntry, error) { return l.fs.ReadDir(l.prefix + name) }
|
func (l rewriteFS) ReadDir(name string) ([]fs.DirEntry, error) { return l.fs.ReadDir(l.prefix + name) }
|
||||||
func (l rewriteFS) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile(l.prefix + name) }
|
func (l rewriteFS) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile(l.prefix + name) }
|
||||||
|
|
||||||
func loadLocalFS() {
|
func loadFilesystems() {
|
||||||
langFS = rewriteFS{laFS, "lang/"}
|
langFS = rewriteFS{laFS, "lang/"}
|
||||||
localFS = rewriteFS{loFS, "data/"}
|
localFS = rewriteFS{loFS, "data/"}
|
||||||
log.Println("Using internal storage")
|
log.Println("Using internal storage")
|
||||||
@ -53,7 +53,7 @@ import (
|
|||||||
var localFS fs.FS
|
var localFS fs.FS
|
||||||
var langFS fs.FS
|
var langFS fs.FS
|
||||||
|
|
||||||
func loadLocalFS() {
|
func loadFilesystems() {
|
||||||
log.Println("Using external storage")
|
log.Println("Using external storage")
|
||||||
executable, _ := os.Executable()
|
executable, _ := os.Executable()
|
||||||
localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data"))
|
localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data"))
|
||||||
|
166
main.go
166
main.go
@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
@ -22,20 +21,27 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-contrib/pprof"
|
|
||||||
"github.com/gin-contrib/static"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/hrfee/jfa-go/common"
|
"github.com/hrfee/jfa-go/common"
|
||||||
_ "github.com/hrfee/jfa-go/docs"
|
_ "github.com/hrfee/jfa-go/docs"
|
||||||
"github.com/hrfee/jfa-go/mediabrowser"
|
"github.com/hrfee/jfa-go/mediabrowser"
|
||||||
"github.com/hrfee/jfa-go/ombi"
|
"github.com/hrfee/jfa-go/ombi"
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
"github.com/logrusorgru/aurora/v3"
|
"github.com/logrusorgru/aurora/v3"
|
||||||
swaggerFiles "github.com/swaggo/files"
|
|
||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
PLATFORM string = runtime.GOOS
|
||||||
|
SOCK string = "jfa-go.sock"
|
||||||
|
SRV *http.Server
|
||||||
|
RESTART chan bool
|
||||||
|
DATA, CONFIG, HOST *string
|
||||||
|
PORT *int
|
||||||
|
DEBUG *bool
|
||||||
|
TEST bool
|
||||||
|
SWAGGER *bool
|
||||||
|
)
|
||||||
|
|
||||||
var serverTypes = map[string]string{
|
var serverTypes = map[string]string{
|
||||||
"jellyfin": "Jellyfin",
|
"jellyfin": "Jellyfin",
|
||||||
"emby": "Emby (experimental)",
|
"emby": "Emby (experimental)",
|
||||||
@ -81,31 +87,6 @@ type appContext struct {
|
|||||||
URLBase string
|
URLBase string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) loadHTML(router *gin.Engine) {
|
|
||||||
customPath := app.config.Section("files").Key("html_templates").MustString("")
|
|
||||||
templatePath := "html"
|
|
||||||
htmlFiles, err := fs.ReadDir(localFS, templatePath)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
loadFiles := make([]string, len(htmlFiles))
|
|
||||||
for i, f := range htmlFiles {
|
|
||||||
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) {
|
|
||||||
app.debug.Printf("Using default \"%s\"", f.Name())
|
|
||||||
loadFiles[i] = filepath.Join(templatePath, f.Name())
|
|
||||||
} else {
|
|
||||||
app.info.Printf("Using custom \"%s\"", f.Name())
|
|
||||||
loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tmpl, err := template.ParseFS(localFS, loadFiles...)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Fatalf("Failed to load templates: %v", err)
|
|
||||||
}
|
|
||||||
router.SetHTMLTemplate(tmpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSecret(length int) (string, error) {
|
func generateSecret(length int) (string, error) {
|
||||||
bytes := make([]byte, length)
|
bytes := make([]byte, length)
|
||||||
_, err := rand.Read(bytes)
|
_, err := rand.Read(bytes)
|
||||||
@ -115,46 +96,6 @@ func generateSecret(length int) (string, error) {
|
|||||||
return base64.URLEncoding.EncodeToString(bytes), err
|
return base64.URLEncoding.EncodeToString(bytes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func setGinLogger(router *gin.Engine, debugMode bool) {
|
|
||||||
if debugMode {
|
|
||||||
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
|
||||||
return fmt.Sprintf("[GIN/DEBUG] %s: %s(%s) => %d in %s; %s\n",
|
|
||||||
param.TimeStamp.Format("15:04:05"),
|
|
||||||
param.Method,
|
|
||||||
param.Path,
|
|
||||||
param.StatusCode,
|
|
||||||
param.Latency,
|
|
||||||
func() string {
|
|
||||||
if param.ErrorMessage != "" {
|
|
||||||
return "Error: " + param.ErrorMessage
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}(),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
|
||||||
return fmt.Sprintf("[GIN] %s(%s) => %d\n",
|
|
||||||
param.Method,
|
|
||||||
param.Path,
|
|
||||||
param.StatusCode,
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
PLATFORM string = runtime.GOOS
|
|
||||||
SOCK string = "jfa-go.sock"
|
|
||||||
SRV *http.Server
|
|
||||||
RESTART chan bool
|
|
||||||
DATA, CONFIG, HOST *string
|
|
||||||
PORT *int
|
|
||||||
DEBUG *bool
|
|
||||||
TEST bool
|
|
||||||
SWAGGER *bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func test(app *appContext) {
|
func test(app *appContext) {
|
||||||
fmt.Printf("\n\n----\n\n")
|
fmt.Printf("\n\n----\n\n")
|
||||||
settings := map[string]interface{}{
|
settings := map[string]interface{}{
|
||||||
@ -191,18 +132,16 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app := new(appContext)
|
app := new(appContext)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
set default config, data and local paths
|
set default config and data paths
|
||||||
also, confusing naming here. data_path is not the internal 'data' directory, rather the users .config/jfa-go folder.
|
data: Contains invites.json, emails.json, user_profile.json, etc.
|
||||||
localFS/data is the internal 'data' directory.
|
config: config.ini. Usually in data, but can be changed via -config.
|
||||||
|
localFS is jfa-go's internal data. On external builds, the directory is named "data" and placed next to the executable.
|
||||||
*/
|
*/
|
||||||
userConfigDir, _ := os.UserConfigDir()
|
userConfigDir, _ := os.UserConfigDir()
|
||||||
app.dataPath = filepath.Join(userConfigDir, "jfa-go")
|
app.dataPath = filepath.Join(userConfigDir, "jfa-go")
|
||||||
app.configPath = filepath.Join(app.dataPath, "config.ini")
|
app.configPath = filepath.Join(app.dataPath, "config.ini")
|
||||||
// executable, _ := os.Executable()
|
|
||||||
// localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data"))
|
|
||||||
// langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang"))
|
|
||||||
app.systemFS = os.DirFS("/")
|
app.systemFS = os.DirFS("/")
|
||||||
// wfs := os.DirFS(filepath.Join(filepath.Dir(executable), "data", "web"))
|
// gin-static doesn't just take a plain http.FileSystem, so we implement it's ServeFileSystem. See static.go.
|
||||||
app.webFS = httpFS{
|
app.webFS = httpFS{
|
||||||
hfs: http.FS(localFS),
|
hfs: http.FS(localFS),
|
||||||
fs: localFS,
|
fs: localFS,
|
||||||
@ -582,74 +521,17 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
app.info.Println("Initializing router")
|
||||||
|
router := app.loadRouter(address, debugMode)
|
||||||
app.info.Println("Loading routes")
|
app.info.Println("Loading routes")
|
||||||
if debugMode {
|
|
||||||
gin.SetMode(gin.DebugMode)
|
|
||||||
} else {
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
}
|
|
||||||
router := gin.New()
|
|
||||||
|
|
||||||
setGinLogger(router, debugMode)
|
|
||||||
|
|
||||||
router.Use(gin.Recovery())
|
|
||||||
app.loadHTML(router)
|
|
||||||
router.Use(static.Serve("/", app.webFS))
|
|
||||||
router.NoRoute(app.NoRouteHandler)
|
|
||||||
if debugMode {
|
|
||||||
app.debug.Println("Loading pprof")
|
|
||||||
pprof.Register(router)
|
|
||||||
}
|
|
||||||
router.GET("/lang/:page", app.GetLanguages)
|
router.GET("/lang/:page", app.GetLanguages)
|
||||||
if !firstRun {
|
if !firstRun {
|
||||||
router.GET("/", app.AdminPage)
|
app.loadRoutes(router)
|
||||||
router.GET("/accounts", app.AdminPage)
|
|
||||||
router.GET("/settings", app.AdminPage)
|
|
||||||
router.GET("/lang/:page/:file", app.ServeLang)
|
|
||||||
router.GET("/token/login", app.getTokenLogin)
|
|
||||||
router.GET("/token/refresh", app.getTokenRefresh)
|
|
||||||
router.POST("/newUser", app.NewUser)
|
|
||||||
router.Use(static.Serve("/invite/", app.webFS))
|
|
||||||
router.GET("/invite/:invCode", app.InviteProxy)
|
|
||||||
if *SWAGGER {
|
|
||||||
app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
|
||||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
|
||||||
}
|
|
||||||
api := router.Group("/", app.webAuth())
|
|
||||||
router.POST("/logout", app.Logout)
|
|
||||||
api.DELETE("/users", app.DeleteUser)
|
|
||||||
api.GET("/users", app.GetUsers)
|
|
||||||
api.POST("/users", app.NewUserAdmin)
|
|
||||||
api.POST("/invites", app.GenerateInvite)
|
|
||||||
api.GET("/invites", app.GetInvites)
|
|
||||||
api.DELETE("/invites", app.DeleteInvite)
|
|
||||||
api.POST("/invites/profile", app.SetProfile)
|
|
||||||
api.GET("/profiles", app.GetProfiles)
|
|
||||||
api.POST("/profiles/default", app.SetDefaultProfile)
|
|
||||||
api.POST("/profiles", app.CreateProfile)
|
|
||||||
api.DELETE("/profiles", app.DeleteProfile)
|
|
||||||
api.POST("/invites/notify", app.SetNotify)
|
|
||||||
api.POST("/users/emails", app.ModifyEmails)
|
|
||||||
// api.POST("/setDefaults", app.SetDefaults)
|
|
||||||
api.POST("/users/settings", app.ApplySettings)
|
|
||||||
api.GET("/config", app.GetConfig)
|
|
||||||
api.POST("/config", app.ModifyConfig)
|
|
||||||
api.POST("/restart", app.restart)
|
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
|
||||||
api.GET("/ombi/users", app.OmbiUsers)
|
|
||||||
api.POST("/ombi/defaults", app.SetOmbiDefaults)
|
|
||||||
}
|
|
||||||
app.info.Printf("Starting router @ %s", address)
|
app.info.Printf("Starting router @ %s", address)
|
||||||
} else {
|
} else {
|
||||||
router.GET("/", app.ServeSetup)
|
app.loadSetup(router)
|
||||||
router.POST("/jellyfin/test", app.TestJF)
|
|
||||||
router.POST("/config", app.ModifyConfig)
|
|
||||||
app.info.Printf("Loading setup @ %s", address)
|
app.info.Printf("Loading setup @ %s", address)
|
||||||
}
|
}
|
||||||
SRV = &http.Server{
|
|
||||||
Addr: address,
|
|
||||||
Handler: router,
|
|
||||||
}
|
|
||||||
go func() {
|
go func() {
|
||||||
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
||||||
cert := app.config.Section("advanced").Key("tls_cert").MustString("")
|
cert := app.config.Section("advanced").Key("tls_cert").MustString("")
|
||||||
@ -671,9 +553,9 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
for range RESTART {
|
for range RESTART {
|
||||||
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := SRV.Shutdown(cntx); err != nil {
|
if err := SRV.Shutdown(ctx); err != nil {
|
||||||
app.err.Fatalf("Server shutdown error: %s", err)
|
app.err.Fatalf("Server shutdown error: %s", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -752,7 +634,7 @@ func main() {
|
|||||||
if flagPassed("test") {
|
if flagPassed("test") {
|
||||||
TEST = true
|
TEST = true
|
||||||
}
|
}
|
||||||
loadLocalFS()
|
loadFilesystems()
|
||||||
if flagPassed("start") {
|
if flagPassed("start") {
|
||||||
args := []string{}
|
args := []string{}
|
||||||
for i, f := range os.Args {
|
for i, f := range os.Args {
|
||||||
|
142
router.go
Normal file
142
router.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/pprof"
|
||||||
|
"github.com/gin-contrib/static"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/logrusorgru/aurora/v3"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loads HTML templates. If [files]/html_templates is set, alternative files inside the directory are loaded in place of the internal templates.
|
||||||
|
func (app *appContext) loadHTML(router *gin.Engine) {
|
||||||
|
customPath := app.config.Section("files").Key("html_templates").MustString("")
|
||||||
|
templatePath := "html"
|
||||||
|
htmlFiles, err := fs.ReadDir(localFS, templatePath)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadFiles := make([]string, len(htmlFiles))
|
||||||
|
for i, f := range htmlFiles {
|
||||||
|
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) {
|
||||||
|
app.debug.Printf("Using default \"%s\"", f.Name())
|
||||||
|
loadFiles[i] = filepath.Join(templatePath, f.Name())
|
||||||
|
} else {
|
||||||
|
app.info.Printf("Using custom \"%s\"", f.Name())
|
||||||
|
loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmpl, err := template.ParseFS(localFS, loadFiles...)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Fatalf("Failed to load templates: %v", err)
|
||||||
|
}
|
||||||
|
router.SetHTMLTemplate(tmpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sets gin logger.
|
||||||
|
func setGinLogger(router *gin.Engine, debugMode bool) {
|
||||||
|
if debugMode {
|
||||||
|
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||||
|
return fmt.Sprintf("[GIN/DEBUG] %s: %s(%s) => %d in %s; %s\n",
|
||||||
|
param.TimeStamp.Format("15:04:05"),
|
||||||
|
param.Method,
|
||||||
|
param.Path,
|
||||||
|
param.StatusCode,
|
||||||
|
param.Latency,
|
||||||
|
func() string {
|
||||||
|
if param.ErrorMessage != "" {
|
||||||
|
return "Error: " + param.ErrorMessage
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}(),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||||
|
return fmt.Sprintf("[GIN] %s(%s) => %d\n",
|
||||||
|
param.Method,
|
||||||
|
param.Path,
|
||||||
|
param.StatusCode,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
|
||||||
|
if debug {
|
||||||
|
gin.SetMode(gin.DebugMode)
|
||||||
|
} else {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
router := gin.New()
|
||||||
|
|
||||||
|
setGinLogger(router, debug)
|
||||||
|
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
app.loadHTML(router)
|
||||||
|
router.Use(static.Serve("/", app.webFS))
|
||||||
|
router.NoRoute(app.NoRouteHandler)
|
||||||
|
if debug {
|
||||||
|
app.debug.Println("Loading pprof")
|
||||||
|
pprof.Register(router)
|
||||||
|
}
|
||||||
|
SRV = &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||||
|
router.GET("/", app.AdminPage)
|
||||||
|
router.GET("/accounts", app.AdminPage)
|
||||||
|
router.GET("/settings", app.AdminPage)
|
||||||
|
router.GET("/lang/:page/:file", app.ServeLang)
|
||||||
|
router.GET("/token/login", app.getTokenLogin)
|
||||||
|
router.GET("/token/refresh", app.getTokenRefresh)
|
||||||
|
router.POST("/newUser", app.NewUser)
|
||||||
|
router.Use(static.Serve("/invite/", app.webFS))
|
||||||
|
router.GET("/invite/:invCode", app.InviteProxy)
|
||||||
|
if *SWAGGER {
|
||||||
|
app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||||
|
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
}
|
||||||
|
api := router.Group("/", app.webAuth())
|
||||||
|
router.POST("/logout", app.Logout)
|
||||||
|
api.DELETE("/users", app.DeleteUser)
|
||||||
|
api.GET("/users", app.GetUsers)
|
||||||
|
api.POST("/users", app.NewUserAdmin)
|
||||||
|
api.POST("/invites", app.GenerateInvite)
|
||||||
|
api.GET("/invites", app.GetInvites)
|
||||||
|
api.DELETE("/invites", app.DeleteInvite)
|
||||||
|
api.POST("/invites/profile", app.SetProfile)
|
||||||
|
api.GET("/profiles", app.GetProfiles)
|
||||||
|
api.POST("/profiles/default", app.SetDefaultProfile)
|
||||||
|
api.POST("/profiles", app.CreateProfile)
|
||||||
|
api.DELETE("/profiles", app.DeleteProfile)
|
||||||
|
api.POST("/invites/notify", app.SetNotify)
|
||||||
|
api.POST("/users/emails", app.ModifyEmails)
|
||||||
|
api.POST("/users/settings", app.ApplySettings)
|
||||||
|
api.GET("/config", app.GetConfig)
|
||||||
|
api.POST("/config", app.ModifyConfig)
|
||||||
|
api.POST("/restart", app.restart)
|
||||||
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
|
api.GET("/ombi/users", app.OmbiUsers)
|
||||||
|
api.POST("/ombi/defaults", app.SetOmbiDefaults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) loadSetup(router *gin.Engine) {
|
||||||
|
router.GET("/", app.ServeSetup)
|
||||||
|
router.POST("/jellyfin/test", app.TestJF)
|
||||||
|
router.POST("/config", app.ModifyConfig)
|
||||||
|
}
|
22
views.go
22
views.go
@ -43,14 +43,19 @@ func (app *appContext) pushResources(gc *gin.Context, admin bool) {
|
|||||||
gc.Header("Link", cssHeader)
|
gc.Header("Link", cssHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) AdminPage(gc *gin.Context) {
|
func (app *appContext) getLang(gc *gin.Context, chosen string) string {
|
||||||
app.pushResources(gc, true)
|
|
||||||
lang := gc.Query("lang")
|
lang := gc.Query("lang")
|
||||||
if lang == "" {
|
if lang == "" {
|
||||||
lang = app.storage.lang.chosenAdminLang
|
lang = chosen
|
||||||
} else if _, ok := app.storage.lang.Admin[lang]; !ok {
|
} else if _, ok := app.storage.lang.Admin[lang]; !ok {
|
||||||
lang = app.storage.lang.chosenAdminLang
|
lang = chosen
|
||||||
}
|
}
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) AdminPage(gc *gin.Context) {
|
||||||
|
app.pushResources(gc, true)
|
||||||
|
lang := app.getLang(gc, app.storage.lang.chosenAdminLang)
|
||||||
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
||||||
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
||||||
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||||
@ -73,12 +78,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
|||||||
func (app *appContext) InviteProxy(gc *gin.Context) {
|
func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||||
app.pushResources(gc, false)
|
app.pushResources(gc, false)
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
lang := gc.Query("lang")
|
lang := app.getLang(gc, app.storage.lang.chosenFormLang)
|
||||||
if lang == "" {
|
|
||||||
lang = app.storage.lang.chosenFormLang
|
|
||||||
} else if _, ok := app.storage.lang.Form[lang]; !ok {
|
|
||||||
lang = app.storage.lang.chosenFormLang
|
|
||||||
}
|
|
||||||
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
|
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
|
||||||
// if app.checkInvite(code, false, "") {
|
// if app.checkInvite(code, false, "") {
|
||||||
inv, ok := app.storage.invites[code]
|
inv, ok := app.storage.invites[code]
|
||||||
@ -89,7 +89,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if key := gc.Query("key"); key != "" {
|
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||||
validKey := false
|
validKey := false
|
||||||
keyIndex := -1
|
keyIndex := -1
|
||||||
for i, k := range inv.Keys {
|
for i, k := range inv.Keys {
|
||||||
|
Loading…
Reference in New Issue
Block a user