1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-10-18 09:00:11 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
b3d7bc7704
refactor, should have been in main 2021-02-02 18:09:02 +00:00
d9354c7d6b
use embed.fs wrapper on data 2021-02-02 15:44:30 +00:00
bfab71c24b
use embed.fs wrapper for langFS so lang/ is not needed in paths
[files]lang_files is now the path to the lang directory, not path to a
directory containing it.
2021-02-02 15:19:43 +00:00
10 changed files with 316 additions and 339 deletions

View File

@ -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
View File

@ -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()

View File

@ -15,7 +15,7 @@ var emailEnabled = false
func (app *appContext) GetPath(sect, key string) (fs.FS, string) { func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
val := app.config.Section(sect).Key(key).MustString("") val := app.config.Section(sect).Key(key).MustString("")
if strings.HasPrefix(val, "jfa-go:") { if strings.HasPrefix(val, "jfa-go:") {
return localFS, "data/" + strings.TrimPrefix(val, "jfa-go:") return localFS, strings.TrimPrefix(val, "jfa-go:")
} }
return app.systemFS, val return app.systemFS, val
} }

View File

@ -867,7 +867,7 @@
"requires_restart": true, "requires_restart": true,
"type": "text", "type": "text",
"value": "", "value": "",
"description": "The path to a directory CONTAINING a 'lang/' directory, which follow the same form as the internal one. See GitHub for more info." "description": "The path to a directory which following the same form as the internal 'lang/' directory. See GitHub for more info."
} }
} }
} }

136
email.go
View File

@ -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,15 +182,8 @@ 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)
tpl, err := template.ParseFS(filesystem, fpath)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"helloUser": emailer.lang.Strings.format("helloUser", username), "helloUser": emailer.lang.Strings.format("helloUser", username),
"clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"), "clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
@ -179,12 +194,6 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
if err != nil { if err != nil {
return nil, err return nil, err
} }
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
}
return email, nil return email, nil
} }
@ -197,15 +206,8 @@ 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)
tpl, err := template.ParseFS(filesystem, fpath)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"hello": emailer.lang.InviteEmail.get("hello"), "hello": emailer.lang.InviteEmail.get("hello"),
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"), "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
"toJoin": emailer.lang.InviteEmail.get("toJoin"), "toJoin": emailer.lang.InviteEmail.get("toJoin"),
@ -217,12 +219,6 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
if err != nil { if err != nil {
return nil, err return nil, err
} }
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
}
return email, nil return email, nil
} }
@ -231,14 +227,8 @@ 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)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"), "inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
"expiredAt": emailer.lang.InviteExpiry.format("expiredAt", "\""+code+"\"", expiry), "expiredAt": emailer.lang.InviteExpiry.format("expiredAt", "\""+code+"\"", expiry),
"notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"), "notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"),
@ -246,12 +236,6 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
if err != nil { if err != nil {
return nil, err return nil, err
} }
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
}
return email, nil return email, nil
} }
@ -266,14 +250,8 @@ 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)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""), "aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""),
"name": emailer.lang.Strings.get("name"), "name": emailer.lang.Strings.get("name"),
"address": emailer.lang.Strings.get("emailAddress"), "address": emailer.lang.Strings.get("emailAddress"),
@ -286,12 +264,6 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
if err != nil { if err != nil {
return nil, err return nil, err
} }
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
}
return email, nil return email, nil
} }
@ -301,14 +273,8 @@ 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)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"helloUser": emailer.lang.Strings.format("helloUser", pwr.Username), "helloUser": emailer.lang.Strings.format("helloUser", pwr.Username),
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"), "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
"ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"), "ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"),
@ -321,12 +287,6 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema
if err != nil { if err != nil {
return nil, err return nil, err
} }
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
}
return email, nil return email, nil
} }
@ -334,14 +294,8 @@ 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)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"), "yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reason": emailer.lang.UserDeleted.get("reason"), "reason": emailer.lang.UserDeleted.get("reason"),
"reasonVal": reason, "reasonVal": reason,
@ -349,12 +303,6 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email
if err != nil { if err != nil {
return nil, err return nil, err
} }
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
}
return email, nil return email, nil
} }
@ -362,14 +310,8 @@ 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)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"welcome": emailer.lang.WelcomeEmail.get("welcome"), "welcome": emailer.lang.WelcomeEmail.get("welcome"),
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), "youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
"jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"), "jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"),
@ -381,12 +323,6 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext) (*Ema
if err != nil { if err != nil {
return nil, err return nil, err
} }
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
}
return email, nil return email, nil
} }

View File

@ -14,16 +14,31 @@ with open("embed.go", "w") as f:
f.write("""package main f.write("""package main
import ( import (
"embed" "embed"
"io/fs"
"log" "log"
) )
//go:embed data data/html data/web data/web/css data/web/js //go:embed data data/html data/web data/web/css data/web/js
var localFS embed.FS var loFS embed.FS
//go:embed lang/common lang/admin lang/email lang/form lang/setup //go:embed lang/common lang/admin lang/email lang/form lang/setup
var langFS embed.FS var laFS embed.FS
func loadLocalFS() { var langFS rewriteFS
var localFS rewriteFS
type rewriteFS struct {
fs embed.FS
prefix string
}
func (l rewriteFS) Open(name string) (fs.File, error) { return l.fs.Open(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 loadFilesystems() {
langFS = rewriteFS{laFS, "lang/"}
localFS = rewriteFS{loFS, "data/"}
log.Println("Using internal storage") log.Println("Using internal storage")
}""") }""")
elif EMBED in falses: elif EMBED in falses:
@ -38,9 +53,9 @@ 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.Dir(executable)) localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data"))
langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang"))
}""") }""")

180
main.go
View File

@ -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 := "data/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,
@ -263,7 +202,7 @@ func start(asDaemon, firstCall bool) {
} }
if _, err := os.Stat(app.configPath); os.IsNotExist(err) { if _, err := os.Stat(app.configPath); os.IsNotExist(err) {
firstRun = true firstRun = true
dConfig, err := fs.ReadFile(localFS, "data/config-default.ini") dConfig, err := fs.ReadFile(localFS, "config-default.ini")
if err != nil { if err != nil {
app.err.Fatalf("Couldn't find default config file") app.err.Fatalf("Couldn't find default config file")
} }
@ -338,10 +277,10 @@ func start(asDaemon, firstCall bool) {
}() }()
} }
app.storage.lang.CommonPath = "lang/common" app.storage.lang.CommonPath = "common"
app.storage.lang.FormPath = "lang/form" app.storage.lang.FormPath = "form"
app.storage.lang.AdminPath = "lang/admin" app.storage.lang.AdminPath = "admin"
app.storage.lang.EmailPath = "lang/email" app.storage.lang.EmailPath = "email"
externalLang := app.config.Section("files").Key("lang_files").MustString("") externalLang := app.config.Section("files").Key("lang_files").MustString("")
var err error var err error
if externalLang == "" { if externalLang == "" {
@ -427,7 +366,7 @@ func start(asDaemon, firstCall bool) {
} }
app.configBasePath = "data/config-base.json" app.configBasePath = "config-base.json"
configBase, _ := fs.ReadFile(localFS, app.configBasePath) configBase, _ := fs.ReadFile(localFS, app.configBasePath)
json.Unmarshal(configBase, &app.configBase) json.Unmarshal(configBase, &app.configBase)
@ -576,80 +515,23 @@ func start(asDaemon, firstCall bool) {
} else { } else {
debugMode = false debugMode = false
address = "0.0.0.0:8056" address = "0.0.0.0:8056"
app.storage.lang.SetupPath = "lang/setup" app.storage.lang.SetupPath = "setup"
err := app.storage.loadLangSetup(langFS) err := app.storage.loadLangSetup(langFS)
if err != nil { if err != nil {
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
View 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)
}

View File

@ -14,12 +14,12 @@ type httpFS struct {
} }
func (f httpFS) Open(name string) (http.File, error) { func (f httpFS) Open(name string) (http.File, error) {
return f.hfs.Open("data/web" + name) return f.hfs.Open("web" + name)
} }
func (f httpFS) Exists(prefix string, filepath string) bool { func (f httpFS) Exists(prefix string, filepath string) bool {
if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) { if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
stats, err := fs.Stat(f.fs, "data/web/"+p) stats, err := fs.Stat(f.fs, "web/"+p)
if err != nil { if err != nil {
return false return false
} }

View File

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