From ea99966057d7bdfd96d5f23129ee1e53be7c5fb5 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 2 Feb 2021 18:09:02 +0000 Subject: [PATCH] refactor, move route loading to router.go --- README.md | 5 +- api.go | 3 + email.go | 262 ++++++++++++++++++---------------------------- embed.py | 4 +- main.go | 186 +++++--------------------------- package-lock.json | 6 +- package.json | 1 + router.go | 156 +++++++++++++++++++++++++++ views.go | 22 ++-- 9 files changed, 301 insertions(+), 344 deletions(-) create mode 100644 router.go diff --git a/README.md b/README.md index 3658e49..78378b8 100644 --- a/README.md +++ b/README.md @@ -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/). -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 linux users, you can place them inside `/opt/jfa-go` and then run -`sudo ln -s /opt/jfa-go/jfa-go /usr/bin/jfa-go` to place it in your PATH. +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 \*nix/macOS users, `chmod +x jfa-go` then place it somewhere in your PATH like `/usr/bin`. Run the executable to start. diff --git a/api.go b/api.go index 3885c44..cf051e2 100644 --- a/api.go +++ b/api.go @@ -1330,6 +1330,9 @@ func (app *appContext) GetLanguages(gc *gin.Context) { gc.JSON(200, resp) } +// @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() diff --git a/email.go b/email.go index 6cfc788..b75197c 100644 --- a/email.go +++ b/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) { email := &Email{ 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() inviteLink := app.config.Section("invite_emails").Key("url_base").String() inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key) - - for _, key := range []string{"html", "text"} { - 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), - "clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"), - "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() - } + var err error + email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", map[string]interface{}{ + "helloUser": emailer.lang.Strings.format("helloUser", username), + "clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"), + "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), + "urlVal": inviteLink, + "confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"), + "message": message, + }) + if err != nil { + return nil, err } 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() inviteLink := app.config.Section("invite_emails").Key("url_base").String() inviteLink = fmt.Sprintf("%s/%s", inviteLink, code) - - for _, key := range []string{"html", "text"} { - 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"), - "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"), - "toJoin": emailer.lang.InviteEmail.get("toJoin"), - "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() - } + var err error + email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", map[string]interface{}{ + "hello": emailer.lang.InviteEmail.get("hello"), + "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"), + "toJoin": emailer.lang.InviteEmail.get("toJoin"), + "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 } return email, nil } @@ -231,26 +227,14 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont subject: emailer.lang.InviteExpiry.get("title"), } expiry := app.formatDatetime(invite.ValidTill) - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("notifications", "expiry_"+key) - 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"), - "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() - } + var err error + email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", map[string]interface{}{ + "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 } return email, nil } @@ -266,31 +250,19 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite } else { tplAddress = address } - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("notifications", "created_"+key) - 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+"\""), - "name": emailer.lang.Strings.get("name"), - "address": emailer.lang.Strings.get("emailAddress"), - "time": emailer.lang.UserCreated.get("time"), - "nameVal": username, - "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() - } + var err error + email.html, email.text, err = emailer.construct(app, "notifications", "created_", map[string]interface{}{ + "aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""), + "name": emailer.lang.Strings.get("name"), + "address": emailer.lang.Strings.get("emailAddress"), + "time": emailer.lang.UserCreated.get("time"), + "nameVal": username, + "addressVal": tplAddress, + "timeVal": created, + "notificationNotice": emailer.lang.UserCreated.get("notificationNotice"), + }) + if err != nil { + return nil, err } 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) message := app.config.Section("email").Key("message").String() - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("password_resets", "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", pwr.Username), - "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"), - "ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"), - "codeExpiry": emailer.lang.PasswordReset.format("codeExpiry", d, t, expiresIn), - "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), - "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() - } + var err error + email.html, email.text, err = emailer.construct(app, "password_resets", "email_", map[string]interface{}{ + "helloUser": emailer.lang.Strings.format("helloUser", pwr.Username), + "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"), + "ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"), + "codeExpiry": emailer.lang.PasswordReset.format("codeExpiry", d, t, expiresIn), + "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), + "pin": emailer.lang.PasswordReset.get("pin"), + "pinVal": pwr.Pin, + "message": message, + }) + if err != nil { + return nil, err } return email, nil } @@ -334,26 +294,14 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email email := &Email{ subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")), } - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("deletion", "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{ - "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() - } + var err error + email.html, email.text, err = emailer.construct(app, "deletion", "email_", map[string]interface{}{ + "yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"), + "reason": emailer.lang.UserDeleted.get("reason"), + "reasonVal": reason, + }) + if err != nil { + return nil, err } return email, nil } @@ -362,30 +310,18 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext) (*Ema email := &Email{ subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")), } - for _, key := range []string{"html", "text"} { - filesystem, fpath := app.GetPath("welcome_email", "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{ - "welcome": emailer.lang.WelcomeEmail.get("welcome"), - "youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), - "jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"), - "jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(), - "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() - } + var err error + email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", map[string]interface{}{ + "welcome": emailer.lang.WelcomeEmail.get("welcome"), + "youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), + "jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"), + "jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(), + "username": emailer.lang.Strings.get("username"), + "usernameVal": username, + "message": app.config.Section("email").Key("message").String(), + }) + if err != nil { + return nil, err } return email, nil } diff --git a/embed.py b/embed.py index 80a2a08..5d4586c 100755 --- a/embed.py +++ b/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) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile(l.prefix + name) } -func loadLocalFS() { +func loadFilesystems() { langFS = rewriteFS{laFS, "lang/"} localFS = rewriteFS{loFS, "data/"} log.Println("Using internal storage") @@ -53,7 +53,7 @@ import ( var localFS fs.FS var langFS fs.FS -func loadLocalFS() { +func loadFilesystems() { log.Println("Using external storage") executable, _ := os.Executable() localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data")) diff --git a/main.go b/main.go index a5433dc..5c62827 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,6 @@ import ( "encoding/json" "flag" "fmt" - "html/template" "io" "io/fs" "log" @@ -23,20 +22,27 @@ import ( "strings" "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/docs" "github.com/hrfee/jfa-go/mediabrowser" "github.com/hrfee/jfa-go/ombi" "github.com/lithammer/shortuuid/v3" "github.com/logrusorgru/aurora/v3" - swaggerFiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" "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{ "jellyfin": "Jellyfin", "emby": "Emby (experimental)", @@ -82,31 +88,6 @@ type appContext struct { 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) { bytes := make([]byte, length) _, err := rand.Read(bytes) @@ -116,46 +97,6 @@ func generateSecret(length int) (string, error) { 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) { fmt.Printf("\n\n----\n\n") settings := map[string]interface{}{ @@ -192,18 +133,16 @@ func start(asDaemon, firstCall bool) { app := new(appContext) /* - set default config, data and local paths - also, confusing naming here. data_path is not the internal 'data' directory, rather the users .config/jfa-go folder. - localFS/data is the internal 'data' directory. + set default config and data paths + data: Contains invites.json, emails.json, user_profile.json, etc. + 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() app.dataPath = filepath.Join(userConfigDir, "jfa-go") 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("/") - // 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{ hfs: http.FS(localFS), fs: localFS, @@ -587,92 +526,15 @@ func start(asDaemon, firstCall bool) { // workaround for potentially broken windows mime types mime.AddExtensionType(".js", "application/javascript") + app.info.Println("Initializing router") + router := app.loadRouter(address, debugMode) 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()) - // Move to router.go - routePrefixes := []string{app.URLBase} - if app.URLBase != "" { - routePrefixes = append(routePrefixes, "") - } - for _, p := range routePrefixes { - router.Use(static.Serve(p+"/", app.webFS)) - } - // - app.loadHTML(router) - router.Use(static.Serve("/", app.webFS)) - router.NoRoute(app.NoRouteHandler) - if debugMode { - app.debug.Println("Loading pprof") - pprof.Register(router) - } - for _, p := range routePrefixes { - router.GET(p+"/lang/:page", app.GetLanguages) - } if !firstRun { - // Move to router - for _, p := range routePrefixes { - router.GET(p+"/", app.AdminPage) - router.GET(p+"/accounts", app.AdminPage) - router.GET(p+"/settings", app.AdminPage) - router.GET(p+"/lang/:page/:file", app.ServeLang) - router.GET(p+"/token/login", app.getTokenLogin) - router.GET(p+"/token/refresh", app.getTokenRefresh) - router.POST(p+"/newUser", app.NewUser) - router.Use(static.Serve(p+"/invite/", app.webFS)) - router.GET(p+"/invite/:invCode", app.InviteProxy) - } - if *SWAGGER { - app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) - for _, p := range routePrefixes { - router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - } - } - api := router.Group("/", app.webAuth()) - for _, p := range routePrefixes { - router.POST(p+"/logout", app.Logout) - api.DELETE(p+"/users", app.DeleteUser) - api.GET(p+"/users", app.GetUsers) - api.POST(p+"/users", app.NewUserAdmin) - api.POST(p+"/invites", app.GenerateInvite) - api.GET(p+"/invites", app.GetInvites) - api.DELETE(p+"/invites", app.DeleteInvite) - api.POST(p+"/invites/profile", app.SetProfile) - api.GET(p+"/profiles", app.GetProfiles) - api.POST(p+"/profiles/default", app.SetDefaultProfile) - api.POST(p+"/profiles", app.CreateProfile) - api.DELETE(p+"/profiles", app.DeleteProfile) - api.POST(p+"/invites/notify", app.SetNotify) - api.POST(p+"/users/emails", app.ModifyEmails) - // api.POST(p + "/setDefaults", app.SetDefaults) - api.POST(p+"/users/settings", app.ApplySettings) - api.GET(p+"/config", app.GetConfig) - api.POST(p+"/config", app.ModifyConfig) - api.POST(p+"/restart", app.restart) - if app.config.Section("ombi").Key("enabled").MustBool(false) { - api.GET(p+"/ombi/users", app.OmbiUsers) - api.POST(p+"/ombi/defaults", app.SetOmbiDefaults) - } - } - app.info.Printf("Starting router @ %s", address) + app.loadRoutes(router) } else { - router.GET("/", app.ServeSetup) - router.POST("/jellyfin/test", app.TestJF) - router.POST("/config", app.ModifyConfig) + app.loadSetup(router) app.info.Printf("Loading setup @ %s", address) } - SRV = &http.Server{ - Addr: address, - Handler: router, - } go func() { if app.config.Section("advanced").Key("tls").MustBool(false) { cert := app.config.Section("advanced").Key("tls_cert").MustString("") @@ -694,9 +556,9 @@ func start(asDaemon, firstCall bool) { } }() for range RESTART { - cntx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - if err := SRV.Shutdown(cntx); err != nil { + if err := SRV.Shutdown(ctx); err != nil { app.err.Fatalf("Server shutdown error: %s", err) } return @@ -775,7 +637,7 @@ func main() { if flagPassed("test") { TEST = true } - loadLocalFS() + loadFilesystems() if flagPassed("start") { args := []string{} for i, f := range os.Args { diff --git a/package-lock.json b/package-lock.json index a16ccc7..3affa71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -228,9 +228,9 @@ "integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=" }, "esbuild": { - "version": "0.7.22", - "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.7.22.tgz", - "integrity": "sha1-kUm5A/gSi3xFp1QEbCQZnXa74I4=" + "version": "0.8.44", + "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.44.tgz", + "integrity": "sha1-KnT0j+IFeQgcnY/pm+b7jShIyIc=" }, "escalade": { "version": "3.1.1", diff --git a/package.json b/package.json index 2ea8a8d..1b2feb6 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "homepage": "https://github.com/hrfee/jfa-go#readme", "dependencies": { "a17t": "^0.4.0", + "esbuild": "^0.8.44", "lodash": "^4.17.19", "mjml": "^4.8.0", "remixicon": "^2.5.0", diff --git a/router.go b/router.go new file mode 100644 index 0000000..b6720c3 --- /dev/null +++ b/router.go @@ -0,0 +1,156 @@ +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) { + routePrefixes := []string{app.URLBase} + if app.URLBase != "" { + routePrefixes = append(routePrefixes, "") + } + for _, p := range routePrefixes { + router.GET(p+"/lang/:page", app.GetLanguages) + router.Use(static.Serve(p+"/", app.webFS)) + router.GET(p+"/", app.AdminPage) + router.GET(p+"/accounts", app.AdminPage) + router.GET(p+"/settings", app.AdminPage) + router.GET(p+"/lang/:page/:file", app.ServeLang) + router.GET(p+"/token/login", app.getTokenLogin) + router.GET(p+"/token/refresh", app.getTokenRefresh) + router.POST(p+"/newUser", app.NewUser) + router.Use(static.Serve(p+"/invite/", app.webFS)) + router.GET(p+"/invite/:invCode", app.InviteProxy) + } + if *SWAGGER { + app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) + for _, p := range routePrefixes { + router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + } + } + api := router.Group("/", app.webAuth()) + for _, p := range routePrefixes { + router.POST(p+"/logout", app.Logout) + api.DELETE(p+"/users", app.DeleteUser) + api.GET(p+"/users", app.GetUsers) + api.POST(p+"/users", app.NewUserAdmin) + api.POST(p+"/invites", app.GenerateInvite) + api.GET(p+"/invites", app.GetInvites) + api.DELETE(p+"/invites", app.DeleteInvite) + api.POST(p+"/invites/profile", app.SetProfile) + api.GET(p+"/profiles", app.GetProfiles) + api.POST(p+"/profiles/default", app.SetDefaultProfile) + api.POST(p+"/profiles", app.CreateProfile) + api.DELETE(p+"/profiles", app.DeleteProfile) + api.POST(p+"/invites/notify", app.SetNotify) + api.POST(p+"/users/emails", app.ModifyEmails) + // api.POST(p + "/setDefaults", app.SetDefaults) + api.POST(p+"/users/settings", app.ApplySettings) + api.GET(p+"/config", app.GetConfig) + api.POST(p+"/config", app.ModifyConfig) + api.POST(p+"/restart", app.restart) + if app.config.Section("ombi").Key("enabled").MustBool(false) { + api.GET(p+"/ombi/users", app.OmbiUsers) + api.POST(p+"/ombi/defaults", app.SetOmbiDefaults) + } + } +} + +func (app *appContext) loadSetup(router *gin.Engine) { + router.GET("/lang/:page", app.GetLanguages) + router.GET("/", app.ServeSetup) + router.POST("/jellyfin/test", app.TestJF) + router.POST("/config", app.ModifyConfig) +} diff --git a/views.go b/views.go index 717dd2c..65097e4 100644 --- a/views.go +++ b/views.go @@ -52,14 +52,19 @@ func (app *appContext) pushResources(gc *gin.Context, admin bool) { gc.Header("Link", cssHeader) } -func (app *appContext) AdminPage(gc *gin.Context) { - app.pushResources(gc, true) +func (app *appContext) getLang(gc *gin.Context, chosen string) string { lang := gc.Query("lang") if lang == "" { - lang = app.storage.lang.chosenAdminLang + lang = chosen } 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() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) @@ -82,12 +87,7 @@ func (app *appContext) AdminPage(gc *gin.Context) { func (app *appContext) InviteProxy(gc *gin.Context) { app.pushResources(gc, false) code := gc.Param("invCode") - lang := gc.Query("lang") - if lang == "" { - lang = app.storage.lang.chosenFormLang - } else if _, ok := app.storage.lang.Form[lang]; !ok { - lang = app.storage.lang.chosenFormLang - } + lang := app.getLang(gc, 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. */ // if app.checkInvite(code, false, "") { inv, ok := app.storage.invites[code] @@ -98,7 +98,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { }) return } - if key := gc.Query("key"); key != "" { + if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) { validKey := false keyIndex := -1 for i, k := range inv.Keys {