diff --git a/cmd/userstyles-fonts/main.go b/cmd/userstyles-fonts/main.go index 67b3c14c..72aefb79 100644 --- a/cmd/userstyles-fonts/main.go +++ b/cmd/userstyles-fonts/main.go @@ -12,8 +12,6 @@ import ( "path/filepath" "strings" "time" - - "userstyles.world/modules/config" ) var ( @@ -58,7 +56,7 @@ func getAsset() (Asset, error) { } asset := release.Assets[0] - asset.Path = path.Join(config.DataDir, asset.Name) + asset.Path = path.Join("data", asset.Name) return asset, nil } diff --git a/cmd/userstyles-ts/main.go b/cmd/userstyles-ts/main.go index af638108..3f780d7d 100644 --- a/cmd/userstyles-ts/main.go +++ b/cmd/userstyles-ts/main.go @@ -1,12 +1,10 @@ package main import ( + "log" "os" "github.com/evanw/esbuild/pkg/api" - - "userstyles.world/modules/config" - "userstyles.world/modules/log" ) var ( @@ -31,15 +29,12 @@ func main() { // TODO: Remove this code? if shouldWatch { - // Ensure we're seeing the error messages in stdout. - config.Production = false - log.Initialize() watch = &api.WatchMode{ OnRebuild: func(result api.BuildResult) { if len(result.Errors) > 0 { - log.Info.Printf("Watch build failed: %d errors\n", len(result.Errors)) + log.Printf("Watch build failed: %d errors\n", len(result.Errors)) } else { - log.Info.Printf("Watch build succeeded: %d warnings\n", len(result.Warnings)) + log.Printf("Watch build succeeded: %d warnings\n", len(result.Warnings)) } }, } diff --git a/cmd/userstyles-world/main.go b/cmd/userstyles-world/main.go index 1b317f0d..67bb66c9 100644 --- a/cmd/userstyles-world/main.go +++ b/cmd/userstyles-world/main.go @@ -1,6 +1,8 @@ package main import ( + "flag" + "fmt" "net/http" "os" "os/signal" @@ -34,18 +36,38 @@ import ( ) func main() { + version := flag.Bool("version", false, "print build info") + custom := flag.String("config", "data/config.json", "path to custom config.json") + flag.Parse() + + if *version { + fmt.Println("Go version:", config.GoVersion) + fmt.Println("Git commit:", config.GitCommit) + fmt.Println("Git signature:", config.GitSignature) + os.Exit(0) + } + + err := config.Load(*custom) + if err != nil { + fmt.Printf("Failed to load config: %s\n", err) + os.Exit(1) + } + log.Initialize() - cache.Initialize() + cache.Init() images.CheckVips() util.InitCrypto() + jwtware.Init() + email.Init() validator.Init() database.Initialize() cron.Initialize() + web.Init() app := fiber.New(fiber.Config{ Views: templates.New(http.FS(web.ViewsDir)), ViewsLayout: "layouts/main", - ProxyHeader: config.ProxyRealIP, + ProxyHeader: config.App.ProxyHeader, JSONEncoder: util.JSONEncoder, IdleTimeout: 5 * time.Second, @@ -55,18 +77,22 @@ func main() { email.SetRenderer(app) - if !config.Production { + if !config.App.Production { app.Use(logger.New()) } api.FastRoutes(app) + app.Use(func(c *fiber.Ctx) error { + c.Locals("App", &config.App) + return c.Next() + }) app.Use(core.HSTSMiddleware) app.Use(core.CSPMiddleware) app.Use(core.FlagsMiddleware) app.Use(jwtware.New("user", jwtware.NormalJWTSigning)) - if config.PerformanceMonitor { + if config.App.Profiling { perf := app.Group("/debug") perf.Use(jwtware.Admin) perf.Use(pprof.New()) @@ -91,7 +117,7 @@ func main() { })) // TODO: Investigate how to "truly" inline sourcemaps in Sass. - if !config.Production { + if !config.App.Production { app.Static("/scss", "web/scss") } @@ -99,7 +125,7 @@ func main() { app.Use(core.NotFound) go func() { - if err := app.Listen(config.Port); err != nil { + if err := app.Listen(config.App.Addr); err != nil { log.Warn.Fatal(err) } }() diff --git a/handlers/api/callback.go b/handlers/api/callback.go index 553e687e..213a06b1 100644 --- a/handlers/api/callback.go +++ b/handlers/api/callback.go @@ -35,7 +35,7 @@ func CallbackGet(c *fiber.Ctx) error { if redirectCode != "codeberg" && redirectCode != "gitlab" { service = "github" // Decode the string so we get our actual information back. - code, err := util.DecryptText(redirectCode, util.AEADOAuth, config.ScrambleConfig) + code, err := util.DecryptText(redirectCode, util.AEADOAuth, config.Secrets) if err != nil { log.Warn.Println("Failed to decode prepared text.") return c.Next() @@ -95,7 +95,7 @@ func CallbackGet(c *fiber.Ctx) error { Value: t, Path: "/", Expires: expiration, - Secure: config.Production, + Secure: config.App.Production, HTTPOnly: true, SameSite: fiber.CookieSameSiteLaxMode, }) diff --git a/handlers/api/code.go b/handlers/api/code.go index 3c7a09da..533f845c 100644 --- a/handlers/api/code.go +++ b/handlers/api/code.go @@ -31,7 +31,7 @@ func GetStyleCode(c *fiber.Ctx) error { code := cache.Code.Get(i) if code == nil { - code, err = os.ReadFile(filepath.Join(config.StyleDir, id)) + code, err = os.ReadFile(filepath.Join(config.Storage.StyleDir, id)) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "message": "userstyle not found", diff --git a/handlers/core/dashboard.go b/handlers/core/dashboard.go index e913696c..0866ebc6 100644 --- a/handlers/core/dashboard.go +++ b/handlers/core/dashboard.go @@ -61,7 +61,7 @@ func getSystemStatus() { systemMutex.Lock() defer systemMutex.Unlock() - uptime := time.Since(config.AppUptime).Round(time.Second) + uptime := time.Since(config.App.Started).Round(time.Second) system.Uptime = uptime.String() system.GoRoutines = runtime.NumGoroutine() diff --git a/handlers/core/link.go b/handlers/core/link.go index c6428f88..5ef15e44 100644 --- a/handlers/core/link.go +++ b/handlers/core/link.go @@ -10,13 +10,13 @@ import ( func GetLinkedSite(c *fiber.Ctx) error { switch c.Params("site") { case "discord": - return c.Redirect(config.AppLinkChatDiscord, fiber.StatusSeeOther) + return c.Redirect(config.App.Discord, fiber.StatusSeeOther) case "matrix": - return c.Redirect(config.AppLinkChatMatrix, fiber.StatusSeeOther) + return c.Redirect(config.App.Matrix, fiber.StatusSeeOther) case "opencollective": - return c.Redirect(config.AppLinkOpenCollective, fiber.StatusSeeOther) + return c.Redirect(config.App.OpenCollective, fiber.StatusSeeOther) case "source": - return c.Redirect(config.AppSourceCode, fiber.StatusSeeOther) + return c.Redirect(config.App.GitRepository, fiber.StatusSeeOther) default: u, _ := jwt.User(c) return c.Render("err", fiber.Map{ diff --git a/handlers/core/monitor.go b/handlers/core/monitor.go index e7cbfc70..d1b1169b 100644 --- a/handlers/core/monitor.go +++ b/handlers/core/monitor.go @@ -9,8 +9,6 @@ import ( "userstyles.world/modules/config" ) -var addr = []byte(config.ProxyMonitor) - func Monitor(c *fiber.Ctx) error { u, ok := jwt.User(c) @@ -30,7 +28,7 @@ func Monitor(c *fiber.Ctx) error { } // Proxy requests. - url := addr + url := []byte(config.App.MonitorURL) url = append(url, c.Request().URI().Path()[8:]...) url = append(url, 63) url = append(url, c.Context().URI().QueryArgs().QueryString()...) diff --git a/handlers/core/proxy.go b/handlers/core/proxy.go index c7a88d5f..b306befb 100644 --- a/handlers/core/proxy.go +++ b/handlers/core/proxy.go @@ -26,7 +26,7 @@ var client = http.Client{ } // Make sure it doesn't redirect to a loopback thingy. - if config.Production && util.IsLoopback(string(req.Host)) { + if config.App.Production && util.IsLoopback(string(req.Host)) { return errors.New("*giggles* Mikey Wikey hates you") } return nil @@ -42,7 +42,7 @@ func Proxy(c *fiber.Ctx) error { } // Set resource location and name. - dir := path.Join(config.ProxyDir, path.Clean(t), path.Clean(id)) + dir := path.Join(config.Storage.ProxyDir, path.Clean(t), path.Clean(id)) name := path.Join(dir, url.PathEscape(link)) // Check if image exists. diff --git a/handlers/jwt/jwt.go b/handlers/jwt/jwt.go index 575b1a34..22a00027 100644 --- a/handlers/jwt/jwt.go +++ b/handlers/jwt/jwt.go @@ -33,7 +33,7 @@ var Protected = func(c *fiber.Ctx) error { var Admin = func(c *fiber.Ctx) error { // Bypass checks if monitor is enabled and request is a local IP address. - if config.PerformanceMonitor && util.IsLocal(config.Production, c.IP()) { + if config.App.Profiling && util.IsLocal(config.App.Production, c.IP()) { return c.Next() } diff --git a/handlers/jwt/jwtware.go b/handlers/jwt/jwtware.go index 2492f2c9..34ed5e65 100644 --- a/handlers/jwt/jwtware.go +++ b/handlers/jwt/jwtware.go @@ -10,10 +10,14 @@ import ( ) var ( - JWTSigningKey = []byte(config.JWTSigningKey) + JWTSigningKey []byte SigningMethod = "HS512" ) +func Init() { + JWTSigningKey = []byte(config.Secrets.SessionTokenKey) +} + func New(local string, keyFunction func(t *jwt.Token) (any, error)) fiber.Handler { extractors := []func(c *fiber.Ctx) (string, bool){ jwtFromCookie(fiber.HeaderAuthorization), diff --git a/handlers/oauthProvider/access_token.go b/handlers/oauthProvider/access_token.go index 72ac30a1..93c16000 100644 --- a/handlers/oauthProvider/access_token.go +++ b/handlers/oauthProvider/access_token.go @@ -35,7 +35,7 @@ func TokenPost(c *fiber.Ctx) error { return errorMessage(c, 400, "Incorrect client_secret specified") } - unsealedText, err := util.DecryptText(tCode, util.AEADOAuthp, config.ScrambleConfig) + unsealedText, err := util.DecryptText(tCode, util.AEADOAuthp, config.Secrets) if err != nil { log.Warn.Println("Failed to unseal JWT text:", err.Error()) return errorMessage(c, 500, "Error: Please notify the UserStyles.world admins.") diff --git a/handlers/oauthProvider/authorize.go b/handlers/oauthProvider/authorize.go index 76b61f57..76185bb6 100644 --- a/handlers/oauthProvider/authorize.go +++ b/handlers/oauthProvider/authorize.go @@ -35,7 +35,7 @@ func redirectFunction(c *fiber.Ctx, state, redirectURI string) error { return errorMessage(c, 500, "Error: Please notify the UserStyles.world admins.") } - returnCode := "?code=" + util.EncryptText(jwtToken, util.AEADOAuthp, config.ScrambleConfig) + returnCode := "?code=" + util.EncryptText(jwtToken, util.AEADOAuthp, config.Secrets) if state != "" { returnCode += "&state=" + state } @@ -98,7 +98,7 @@ func AuthorizeGet(c *fiber.Ctx) error { arguments := fiber.Map{ "User": u, "OAuth": oauth, - "SecureToken": util.EncryptText(jwtToken, util.AEADOAuthp, config.ScrambleConfig), + "SecureToken": util.EncryptText(jwtToken, util.AEADOAuthp, config.Secrets), } for _, v := range oauth.Scopes { arguments["Scope_"+v] = true @@ -116,7 +116,7 @@ func AuthPost(c *fiber.Ctx) error { return errorMessage(c, 400, "Incorrect oauthID specified") } - unsealedText, err := util.DecryptText(secureToken, util.AEADOAuthp, config.ScrambleConfig) + unsealedText, err := util.DecryptText(secureToken, util.AEADOAuthp, config.Secrets) if err != nil { log.Warn.Println("Failed to unseal JWT text:", err.Error()) return errorMessage(c, 500, "Error: Please notify the UserStyles.world admins.") diff --git a/handlers/oauthProvider/authorize_style.go b/handlers/oauthProvider/authorize_style.go index 9c4e86a5..7263387c 100644 --- a/handlers/oauthProvider/authorize_style.go +++ b/handlers/oauthProvider/authorize_style.go @@ -46,7 +46,7 @@ func OAuthStyleGet(c *fiber.Ctx) error { log.Warn.Println("Failed to create a JWT Token:", err.Error()) return errorMessage(c, 500, "Couldn't make JWT Token, Error: Please notify the UserStyles.world admins.") } - secureToken := util.EncryptText(jwtToken, util.AEADOAuthp, config.ScrambleConfig) + secureToken := util.EncryptText(jwtToken, util.AEADOAuthp, config.Secrets) styles, err := storage.FindStyleCardsForUsername(u.Username) if err != nil { @@ -83,7 +83,7 @@ func OAuthStylePost(c *fiber.Ctx) error { return errorMessage(c, 400, "Incorrect oauthID specified") } - unsealedText, err := util.DecryptText(secureToken, util.AEADOAuthp, config.ScrambleConfig) + unsealedText, err := util.DecryptText(secureToken, util.AEADOAuthp, config.Secrets) if err != nil { log.Warn.Println("Failed to unseal JWT text:", err.Error()) return errorMessage(c, 500, "Error: Please notify the UserStyles.world admins.") @@ -135,7 +135,7 @@ func OAuthStylePost(c *fiber.Ctx) error { return errorMessage(c, 500, "Error: Please notify the UserStyles.world admins.") } - returnCode := "?code=" + util.EncryptText(jwtToken, util.AEADOAuthp, config.ScrambleConfig) + returnCode := "?code=" + util.EncryptText(jwtToken, util.AEADOAuthp, config.Secrets) returnCode += "&style_id=" + styleID if state != "" { returnCode += "&state=" + state @@ -153,7 +153,7 @@ func OAuthStyleNewPost(c *fiber.Ctx) error { return errorMessage(c, 400, "Incorrect oauthID specified") } - unsealedText, err := util.DecryptText(secureToken, util.AEADOAuthp, config.ScrambleConfig) + unsealedText, err := util.DecryptText(secureToken, util.AEADOAuthp, config.Secrets) if err != nil { log.Warn.Println("Failed to unseal JWT text:", err.Error()) return errorMessage(c, 500, "Error: Please notify the UserStyles.world admins.") diff --git a/handlers/review/remove.go b/handlers/review/remove.go index fd5456d8..a2b5df65 100644 --- a/handlers/review/remove.go +++ b/handlers/review/remove.go @@ -106,7 +106,7 @@ func removeForm(c *fiber.Ctx) error { args := fiber.Map{ "User": r.User, "Log": l, - "Link": config.BaseURL + "/modlog#id-" + strconv.Itoa(int(l.ID)), + "Link": config.App.BaseURL + "/modlog#id-" + strconv.Itoa(int(l.ID)), } title := "Your review has been removed" diff --git a/handlers/style/add.go b/handlers/style/add.go index e57786c8..45906dc4 100644 --- a/handlers/style/add.go +++ b/handlers/style/add.go @@ -117,7 +117,7 @@ func handleAPIStyle(c *fiber.Ctx, secureToken, oauthID, styleID string, style *m }) } - unsealedText, err := util.DecryptText(secureToken, util.AEADOAuthp, config.ScrambleConfig) + unsealedText, err := util.DecryptText(secureToken, util.AEADOAuthp, config.Secrets) if err != nil { log.Warn.Println("Failed to unseal JWT text:", err.Error()) return c.Status(500). @@ -183,7 +183,7 @@ func handleAPIStyle(c *fiber.Ctx, secureToken, oauthID, styleID string, style *m }) } - returnCode := "?code=" + util.EncryptText(jwtToken, util.AEADOAuthp, config.ScrambleConfig) + returnCode := "?code=" + util.EncryptText(jwtToken, util.AEADOAuthp, config.Secrets) returnCode += "&style_id=" + styleID if state != "" { returnCode += "&state=" + state diff --git a/handlers/style/ban.go b/handlers/style/ban.go index 9a5b24be..b24b0d2c 100644 --- a/handlers/style/ban.go +++ b/handlers/style/ban.go @@ -156,7 +156,7 @@ func sendRemovalEmail(user *storage.User, style *models.Style, event *models.Log "User": user, "Style": style, "Log": event, - "Link": config.BaseURL + "/modlog#id-" + strconv.Itoa(int(event.ID)), + "Link": config.App.BaseURL + "/modlog#id-" + strconv.Itoa(int(event.ID)), } title := "Your style has been removed" diff --git a/handlers/style/bulkban.go b/handlers/style/bulkban.go index 55cfff44..326cf862 100644 --- a/handlers/style/bulkban.go +++ b/handlers/style/bulkban.go @@ -134,7 +134,7 @@ func sendBulkRemovalEmail(user *storage.User, styles []*models.Style, event *mod "User": user, "Styles": styles, "Log": event, - "Link": config.BaseURL + "/modlog#id-" + strconv.Itoa(int(event.ID)), + "Link": config.App.BaseURL + "/modlog#id-" + strconv.Itoa(int(event.ID)), } var title string diff --git a/handlers/style/category.go b/handlers/style/category.go index e2a11dd0..ee726226 100644 --- a/handlers/style/category.go +++ b/handlers/style/category.go @@ -33,7 +33,7 @@ func GetCategory(c *fiber.Ctx) error { } c.Locals("Pagination", p) - cat, err := storage.GetStyleCategories(page, config.AppPageMaxItems) + cat, err := storage.GetStyleCategories(page, config.App.PageMaxItems) if err != nil { c.Locals("Title", "Failed to find categories") return c.Render("err", fiber.Map{}) diff --git a/handlers/style/explore.go b/handlers/style/explore.go index d61d407c..39d58757 100644 --- a/handlers/style/explore.go +++ b/handlers/style/explore.go @@ -39,7 +39,7 @@ func GetExplore(c *fiber.Ctx) error { c.Locals("Pagination", p) // Query for [sorted] styles. - s, err := storage.FindStyleCardsPaginated(p.Now, config.AppPageMaxItems, p.SortStyles()) + s, err := storage.FindStyleCardsPaginated(p.Now, config.App.PageMaxItems, p.SortStyles()) if err != nil { log.Database.Println("Failed to get styles:", err) c.Locals("Title", "Styles not found") diff --git a/handlers/style/promote.go b/handlers/style/promote.go index 46efb8d8..e8d1dd3c 100644 --- a/handlers/style/promote.go +++ b/handlers/style/promote.go @@ -17,9 +17,9 @@ func sendPromotionEmail(style *models.APIStyle, user *models.User, mod string) { args := fiber.Map{ "User": user, "Style": style, - "StyleLink": config.BaseURL + "/style/" + strconv.Itoa(int(style.ID)), + "StyleLink": config.App.BaseURL + "/style/" + strconv.Itoa(int(style.ID)), "ModName": mod, - "ModLink": config.BaseURL + "/user/" + mod, + "ModLink": config.App.BaseURL + "/user/" + mod, } title := "Your style has been featured" diff --git a/handlers/style/style.go b/handlers/style/style.go index 4c886068..cd098dc8 100644 --- a/handlers/style/style.go +++ b/handlers/style/style.go @@ -29,7 +29,7 @@ func Routes(app *fiber.App) { r.Post("/styles/ban/:id", jwtware.Protected, BanPost) r.Get("/styles/bulk-ban/:userid", jwtware.Protected, BulkBanGet) r.Post("/styles/bulk-ban/:userid", jwtware.Protected, BulkBanPost) - r.Static("/preview", config.PublicDir, fiber.Static{ + r.Static("/preview", config.Storage.PublicDir, fiber.Static{ MaxAge: 2678400, // 1 month }) } diff --git a/handlers/user/account.go b/handlers/user/account.go index 00941b9d..2091f754 100644 --- a/handlers/user/account.go +++ b/handlers/user/account.go @@ -176,7 +176,7 @@ func EditAccount(c *fiber.Ctx) error { Value: v, Path: "/", Expires: time.Now().Add(time.Hour * 24 * 30 * 6), - Secure: config.Production, + Secure: config.App.Production, HTTPOnly: true, SameSite: fiber.CookieSameSiteLaxMode, } diff --git a/handlers/user/ban.go b/handlers/user/ban.go index 95e7f4b3..2efc6838 100644 --- a/handlers/user/ban.go +++ b/handlers/user/ban.go @@ -117,7 +117,7 @@ func ConfirmBan(c *fiber.Ctx) error { args := fiber.Map{ "User": user, "Reason": logEntry.Reason, - "Link": config.BaseURL + "/modlog#id-" + strconv.Itoa(int(logEntry.ID)), + "Link": config.App.BaseURL + "/modlog#id-" + strconv.Itoa(int(logEntry.ID)), } err = email.Send("user/ban", user.Email, "You have been banned", args) if err != nil { diff --git a/handlers/user/login.go b/handlers/user/login.go index 038aefaa..b4834c67 100644 --- a/handlers/user/login.go +++ b/handlers/user/login.go @@ -102,7 +102,7 @@ func LoginPost(c *fiber.Ctx) error { Value: t, Path: "/", Expires: expiration, - Secure: config.Production, + Secure: config.App.Production, HTTPOnly: true, SameSite: fiber.CookieSameSiteLaxMode, } diff --git a/handlers/user/profile.go b/handlers/user/profile.go index 4f51e6e1..1955e6aa 100644 --- a/handlers/user/profile.go +++ b/handlers/user/profile.go @@ -42,7 +42,7 @@ func Profile(c *fiber.Ctx) error { } c.Locals("Count", count) - size := config.AppPageMaxItems + size := config.App.PageMaxItems p := models.NewPagination(page, count, c.Query("sort"), c.Path()) if p.OutOfBounds() { return c.Redirect(p.URL(p.Now), 302) diff --git a/handlers/user/recover.go b/handlers/user/recover.go index 580c09fe..fffb0d2d 100644 --- a/handlers/user/recover.go +++ b/handlers/user/recover.go @@ -78,7 +78,7 @@ func RecoverPost(c *fiber.Ctx) error { return } - link := config.BaseURL + "/reset/" + util.EncryptText(jwtToken, util.AEADCrypto, config.ScrambleConfig) + link := config.App.BaseURL + "/reset/" + util.EncryptText(jwtToken, util.AEADCrypto, config.Secrets) args := fiber.Map{ "User": user, diff --git a/handlers/user/register.go b/handlers/user/register.go index c276fa07..2ce07bd0 100644 --- a/handlers/user/register.go +++ b/handlers/user/register.go @@ -71,7 +71,7 @@ func RegisterPost(c *fiber.Ctx) error { }) } - link := c.BaseURL() + "/verify/" + util.EncryptText(token, util.AEADCrypto, config.ScrambleConfig) + link := c.BaseURL() + "/verify/" + util.EncryptText(token, util.AEADCrypto, config.Secrets) args := fiber.Map{ "User": u, "Link": link, diff --git a/handlers/user/reset.go b/handlers/user/reset.go index 8b81bfe8..ceb2f766 100644 --- a/handlers/user/reset.go +++ b/handlers/user/reset.go @@ -32,7 +32,7 @@ func ResetGet(c *fiber.Ctx) error { return renderError } - _, err := util.DecryptText(key, util.AEADCrypto, config.ScrambleConfig) + _, err := util.DecryptText(key, util.AEADCrypto, config.Secrets) if err != nil { log.Warn.Println("Failed to unseal JWT text:", err.Error()) return renderError @@ -71,7 +71,7 @@ func ResetPost(c *fiber.Ctx) error { }) } - unSealedText, err := util.DecryptText(key, util.AEADCrypto, config.ScrambleConfig) + unSealedText, err := util.DecryptText(key, util.AEADCrypto, config.Secrets) if err != nil { log.Warn.Println("Failed to unseal JWT text:", err.Error()) return renderError diff --git a/handlers/user/verify.go b/handlers/user/verify.go index eedab023..6b5353b0 100644 --- a/handlers/user/verify.go +++ b/handlers/user/verify.go @@ -28,7 +28,7 @@ func VerifyGet(c *fiber.Ctx) error { }) } - unSealedText, err := util.DecryptText(base64Key, util.AEADCrypto, config.ScrambleConfig) + unSealedText, err := util.DecryptText(base64Key, util.AEADCrypto, config.Secrets) if err != nil { log.Warn.Printf("Failed to decode JWT text: %s\n", err.Error()) return c.Render("err", fiber.Map{ diff --git a/models/pagination.go b/models/pagination.go index d343b774..47ee388a 100644 --- a/models/pagination.go +++ b/models/pagination.go @@ -83,8 +83,8 @@ func (p *Pagination) calcItems(total int) { } // Calculate max page and remainder. - p.Max = total / config.AppPageMaxItems - if total%config.AppPageMaxItems > 0 { + p.Max = total / config.App.PageMaxItems + if total%config.App.PageMaxItems > 0 { p.Max++ } diff --git a/models/style.go b/models/style.go index 6c15041a..a991a38f 100644 --- a/models/style.go +++ b/models/style.go @@ -281,7 +281,7 @@ func (s *Style) UpdateColumn(col string, val any) error { // SetPreview will set preview image URL. func (s *Style) SetPreview() { - s.Preview = fmt.Sprintf("%s/preview/%d/%dt.webp", config.BaseURL, s.ID, s.PreviewVersion) + s.Preview = fmt.Sprintf("%s/preview/%d/%dt.webp", config.App.BaseURL, s.ID, s.PreviewVersion) } var ( @@ -369,7 +369,7 @@ func (s Style) ValidateCode(v *validator.Validate, addPage bool) (string, error) // SetPreview will set preview image URL. func (s *APIStyle) SetPreview() { - s.Preview = fmt.Sprintf("%s/preview/%d/%dt.webp", config.BaseURL, s.ID, s.PreviewVersion) + s.Preview = fmt.Sprintf("%s/preview/%d/%dt.webp", config.App.BaseURL, s.ID, s.PreviewVersion) } // SelectUpdateStyle will update specific fields in the styles table. @@ -386,11 +386,11 @@ func SelectUpdateStyle(s Style) error { } func SaveStyleCode(id, s string) error { - return os.WriteFile(filepath.Join(config.StyleDir, id), []byte(s), 0o644) + return os.WriteFile(filepath.Join(config.Storage.StyleDir, id), []byte(s), 0o644) } func RemoveStyleCode(id string) error { - return os.Remove(filepath.Join(config.StyleDir, id)) + return os.Remove(filepath.Join(config.Storage.StyleDir, id)) } // mirrorEnabled returns whether or not mirroring is enabled. diff --git a/modules/cache/cache.go b/modules/cache/cache.go index 39a10fa2..c73d7b94 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -12,18 +12,18 @@ import ( ) var ( - CacheFile = path.Join(config.CacheDir, "cache") + CacheFile string Store = cache.New(cache.NoExpiration, 5*time.Minute) - Code = newLRU(config.CachedCodeItems, "code") + Code *LRU ) -func init() { +func Init() { dirs := [...]string{ - config.CacheDir, - config.ImageDir, - config.ProxyDir, - config.PublicDir, - config.StyleDir, + config.Storage.CacheDir, + config.Storage.ImageDir, + config.Storage.ProxyDir, + config.Storage.PublicDir, + config.Storage.StyleDir, } // Create dir if it doesn't exist. @@ -43,9 +43,10 @@ func init() { // Run install/view stats. InstallStats.Run() ViewStats.Run() -} -func Initialize() { + CacheFile = path.Join(config.Storage.CacheDir, "cache") + Code = newLRU(config.App.CodeMaxItems, "code") + if err := Store.LoadFile(CacheFile); err != nil { log.Warn.Println("Failed to read cache:", err) } diff --git a/modules/config/config.go b/modules/config/config.go index 1a9fed76..c1b36c93 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -1,97 +1,226 @@ +// Package config provides configuration options. package config import ( + "encoding/json" "fmt" - "path" - "strings" + "log" + "os" "time" ) -type ScrambleSettings struct { - StepSize int - BytesPerInsert int -} +type ( + AppConfig struct { + Debug bool + Profiling bool + Production bool -var ( - GitCommit string - GitVersion string - - Port = getEnv("PORT", ":3000") - BaseURL = getEnv("BASE_URL", "http://localhost"+Port) - DB = getEnv("DB", "dev.db") - DBDebug = getEnv("DB_DEBUG", "silent") - DBColor = getEnvBool("DB_COLOR", false) - DBMigrate = getEnvBool("DB_MIGRATE", false) - DBDrop = getEnvBool("DB_DROP", false) - DBRandomData = getEnvBool("DB_RANDOM_DATA", false) - DBRandomDataAmount = getEnvInt("DB_RANDOM_DATA_AMOUNT", 100) - DBMaxOpenConns = getEnvInt("DB_MAX_OPEN_CONNS", 10) - Salt = getEnvInt("SALT", 10) - JWTSigningKey = getEnv("JWT_SIGNING_KEY", "ABigSecretPassword") - VerifyJWTSigningKey = getEnv("VERIFY_JWT_SIGNING_KEY", "OhNoWeCantUseTheSameAsJWTBeCaUseSeCuRiTy1337") - OAuthpJWTSigningKey = getEnv("OAUTHP_JWT_SIGNING_KEY", "ImNotACatButILikeUnicorns") - CryptoKey = getEnv("CRYPTO_KEY", "ABigSecretPasswordWhichIsExact32") - StatsKey = getEnv("STATS_KEY", "KeyUsedForHashingStatistics") - OAuthKey = getEnv("OAUTH_KEY", "AnotherStringLetstrySomethΦΦΦ") - OAuthpKey = getEnv("OAUTHP_KEY", "(✿◠‿◠^◡^)っ✂❤") - EmailAddress = getEnv("EMAIL_ADDRESS", "test@userstyles.world") - EmailPassword = getEnv("EMAIL_PWD", "hahah_not_your_password") - GitHubClientID = getEnv("GITHUB_CLIENT_ID", "SOmeOneGiVeMeIdEaSwHaTtOpUtHeRe") - GitHubClientSecret = getEnv("GITHUB_CLIENT_SECRET", "OurSecretHere?_www.youtube.com/watch?v=dQw4w9WgXcQ") - GitlabClientID = getEnv("GITLAB_CLIENT_ID", "SOmeOneGiVeMeIdEaSwHaTtOpUtHeRe") - GitlabClientSecret = getEnv("GITLAB_CLIENT_SECRET", "www.youtube.com/watch?v=dQw4w9WgXcQ") - CodebergClientID = getEnv("CODEBERG_CLIENT_ID", "SOmeOneGiVeMeIdEaSwHaTtOpUtHeRe") - CodebergClientSecret = getEnv("CODEBERG_CLIENT_SECRET", "IMgettinggboredd") - PerformanceMonitor = getEnvBool("PERFORMANCE_MONITOR", false) - IMAPServer = getEnv("IMAP_SERVER", "mail.userstyles.world:587") - ProxyMonitor = getEnv("PROXY_MONITOR", "unset") - SearchReindex = getEnvBool("SEARCH_REINDEX", false) - - // Production is used for various "feature flags". - Production = DB != "dev.db" - - ScrambleConfig = &ScrambleSettings{ - StepSize: getEnvInt("NONCE_SCRAMBLE_STEP", 2), - BytesPerInsert: getEnvInt("NONCE_SCRAMBLE_BYTES_PER_INSERT", 3), + Addr string + BaseURL string + MonitorURL string + ProxyHeader string + + Name string + Description string + Codename string + Copyright string + Started time.Time `json:"-"` + EmailRe string + PageMaxItems int + CodeMaxItems int + + // External links. + Discord string + Matrix string + OpenCollective string + GitRepository string + + // Git info. + GitCommitURL string `json:"-"` + GitCommitSHA string `json:"-"` + GitSignature string `json:"-"` } - DataDir = path.Join(getEnv("DATA_DIR", "data")) - CacheDir = path.Join(DataDir, "cache") - ImageDir = path.Join(DataDir, "images") - StyleDir = path.Join(DataDir, "styles") - ProxyDir = path.Join(DataDir, "proxy") - PublicDir = path.Join(DataDir, "public") + DatabaseConfig struct { + Name string + Debug string + Colorful bool + Migrate bool + Drop bool + RandomData bool + RandomDataAmount int + MaxOpenConns int + } - LogFile = path.Join(DataDir, "userstyles.log") + OpenAuthConfig struct { + GitHubID string + GitHubSecret string + GitLabID string + GitLabSecret string + CodebergID string + CodebergSecret string + } - AppName = "UserStyles.world" - AppCodeName = "Fennec Fox" - AppSourceCode = "https://github.com/userstyles-world/userstyles.world" - AppLatestCommit = AppSourceCode + "/commit/" + GitCommit - AppCommitSHA = fmt.Sprintf("%.7s", GitCommit) - AppUptime = time.Now() - AppPageMaxItems = 36 + SecretsConfig struct { + PasswordCost int + ScrambleStepSize int + ScrambleBytesPerInsert int - AppLinkChatDiscord = "https://discord.gg/P5zra4nFS2" - AppLinkChatMatrix = "https://matrix.to/#/#userstyles:matrix.org" - AppLinkOpenCollective = "https://opencollective.com/userstyles" + SessionTokenKey string + RecoverTokenKey string + ProviderTokenKey string - AllowedEmailsRe = `^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$` + CryptoKey string + StatsKey string + OAuthClientKey string + OAuthProviderKey string + EmailAddress string + EmailPassword string + EmailServer string + } + + StorageConfig struct { + DataDir string + CacheDir string + ImageDir string + StyleDir string + ProxyDir string + PublicDir string + LogFile string + } - CachedCodeItems = getEnvInt("CACHED_CODE_ITEMS", 250) - ProxyRealIP = getEnv("PROXY_REAL_IP", "") + config struct { + App AppConfig + Database DatabaseConfig + OpenAuth OpenAuthConfig + Secrets SecretsConfig + Storage StorageConfig + } ) -// OAuthURL returns the proper callback URL depending on the environment. -func OAuthURL() string { - return BaseURL + "/api/callback/" +var ( + // GoVersion is the version of Go compiler used to build this program. + GoVersion string + + // GitCommit is the latest Git commit used to build this program. + GitCommit string + + // GitSignature is the Git version string used to build this program. + GitSignature string + + // App stores general configuration. + App *AppConfig + + // Database stores database configuration. + Database *DatabaseConfig + + // OpenAuth stores configuration needed for connecting to external services. + OpenAuth *OpenAuthConfig + + // Secrets stores cryptographic keys and related configuration. + Secrets *SecretsConfig + + // Storage stores paths to directories and files. + Storage *StorageConfig +) + +func (app *AppConfig) UpdateCopyright() { + if app.Name == "" { + log.Fatal("config: App.Name can't be an empty string") + } + + app.Copyright = fmt.Sprintf("© 2020–%d %s", time.Now().Year(), app.Name) +} + +// UpdateGitInfo updates dynamic Git-specific fields. +func (app *AppConfig) UpdateGitInfo() { + if app.GitRepository == "" { + log.Fatal("config: App.Repository can't be an empty string") + } + + app.GitCommitURL = fmt.Sprintf("%s/commit/%s", app.GitRepository, GitCommit) + app.GitCommitSHA = fmt.Sprintf("%.8s", GitCommit) + app.GitSignature = GitSignature } -// raw tweaks allowed URLs to make them work seamlessly in both environments. -func raw(s string) string { - if !Production { - s += "|userstyles.world" +// defaultConfig is a set of default configs used in development environment. +func defaultConfig() *config { + return &config{ + App: AppConfig{ + Addr: ":3000", + BaseURL: "http://localhost:3000", + Name: "UserStyles.world", + Description: "A free and open-source, community-driven website for browsing and sharing UserCSS userstyles.", + Codename: "Fennec Fox", + Started: time.Now(), + EmailRe: `^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$`, + PageMaxItems: 36, + CodeMaxItems: 250, + + Discord: "https://discord.gg/P5zra4nFS2", + Matrix: "https://matrix.to/#/#userstyles:matrix.org", + OpenCollective: "https://opencollective.com/userstyles", + GitRepository: "https://github.com/userstyles-world/userstyles.world", + }, + Database: DatabaseConfig{ + Name: "dev.db", + Debug: "info", + Colorful: true, + MaxOpenConns: 10, + }, + Secrets: SecretsConfig{ + PasswordCost: 10, + ScrambleStepSize: 2, + ScrambleBytesPerInsert: 3, + SessionTokenKey: "ABigSecretPassword", + RecoverTokenKey: "OhNoWeCantUseTheSameAsJWTBeCaUseSeCuRiTy1337", + ProviderTokenKey: "ImNotACatButILikeUnicorns", + CryptoKey: "ABigSecretPasswordWhichIsExact32", + StatsKey: "KeyUsedForHashingStatistics", + OAuthClientKey: "AnotherStringLetstrySomethΦΦΦ", + OAuthProviderKey: "(✿◠‿◠^◡^)っ✂❤", + }, + Storage: StorageConfig{ + DataDir: "data", + CacheDir: "data/cache", + ImageDir: "data/images", + StyleDir: "data/styles", + ProxyDir: "data/proxy", + PublicDir: "data/public", + LogFile: "data/userstyles.log", + }, } - r := strings.NewReplacer("http://", "", "https://", "") - return r.Replace(s) +} + +// Load tries to load configuration from a given path. +func Load(path string) error { + b, err := os.ReadFile(path) + if err != nil { + return err + } + + c := defaultConfig() + if err = json.Unmarshal(b, &c); err != nil { + return err + } + + c.App.UpdateGitInfo() + c.App.UpdateCopyright() + + if c.App.Debug { + b, err := json.MarshalIndent(c, "", "\t") + if err != nil { + return err + } + + log.Println("config:", string(b)) + } + + App = &c.App + Database = &c.Database + OpenAuth = &c.OpenAuth + Secrets = &c.Secrets + Storage = &c.Storage + + return nil } diff --git a/modules/cron/cron.go b/modules/cron/cron.go index 041d8726..9be55ec9 100644 --- a/modules/cron/cron.go +++ b/modules/cron/cron.go @@ -8,6 +8,7 @@ import ( // "userstyles.world/models" // "userstyles.world/modules/cache" "userstyles.world/modules/cache" + "userstyles.world/modules/config" "userstyles.world/modules/database" "userstyles.world/modules/database/snapshot" "userstyles.world/modules/log" @@ -66,4 +67,11 @@ func Initialize() { if err != nil { log.Warn.Println("Failed to set compact index job:", err) } + + _, err = s.Cron("0 0 1 1 *").Do(func() { + config.App.UpdateCopyright() + }) + if err != nil { + log.Warn.Println("Failed to set update copyright job:", err) + } } diff --git a/modules/database/init/init.go b/modules/database/init/init.go index a5ca69a5..47d27de5 100644 --- a/modules/database/init/init.go +++ b/modules/database/init/init.go @@ -39,12 +39,12 @@ func connect() (*gorm.DB, error) { logger.Config{ SlowThreshold: time.Second, LogLevel: logLevel(), - Colorful: config.DBColor, + Colorful: config.Database.Colorful, }, ), } - file := path.Join(config.DataDir, config.DB) + file := path.Join(config.Storage.DataDir, config.Database.Name) conn, err := gorm.Open(sqlite.Open(file), gormConfig) if err != nil { return nil, err @@ -75,11 +75,11 @@ func Initialize() { } // GORM doesn't set a maximum of open connections by default. - db.SetMaxOpenConns(config.DBMaxOpenConns) + db.SetMaxOpenConns(config.Database.MaxOpenConns) shouldSeed := false // Generate data for development. - if config.DBDrop && !config.Production { + if config.Database.Drop && !config.App.Production { for _, table := range tables { if err := drop(table.model); err != nil { log.Warn.Fatalf("Failed to drop %s, err: %s\n", table.name, err.Error()) @@ -96,7 +96,7 @@ func Initialize() { } // Migrate tables. - if config.DBMigrate { + if config.Database.Migrate { for _, table := range tables { if err := migrate(table.model); err != nil { log.Warn.Fatalf("Failed to migrate %s, err: %s\n", table.name, err.Error()) @@ -115,7 +115,7 @@ func Initialize() { } // TODO: Simplify the entire process, including dropping and seeding data. - if config.DBMigrate { + if config.Database.Migrate { log.Info.Println("Database migration complete.") os.Exit(0) } @@ -279,8 +279,8 @@ func seed() { }, } - if config.DBRandomData { - s, u := generateData(config.DBRandomDataAmount) + if config.Database.RandomData { + s, u := generateData(config.Database.RandomDataAmount) styles = append(styles, s...) users = append(users, u...) } diff --git a/modules/database/init/utils.go b/modules/database/init/utils.go index 4a1d7554..484b1090 100644 --- a/modules/database/init/utils.go +++ b/modules/database/init/utils.go @@ -8,7 +8,7 @@ import ( ) func logLevel() logger.LogLevel { - switch config.DBDebug { + switch config.Database.Debug { case "error": return logger.Error case "warn": diff --git a/modules/email/builder.go b/modules/email/builder.go index 6c1faaba..ec0d0474 100644 --- a/modules/email/builder.go +++ b/modules/email/builder.go @@ -12,10 +12,14 @@ import ( ) var ( - auth = sasl.NewPlainClient("", config.EmailAddress, config.EmailPassword) + auth sasl.Client clrf = "\r\n" ) +func Init() { + auth = sasl.NewPlainClient("", config.Secrets.EmailAddress, config.Secrets.EmailPassword) +} + type EmailBuilder struct { to string from string @@ -136,7 +140,7 @@ func (eb *EmailBuilder) SendEmail(imapServer string) error { eb.boundary = util.RandomString(30) if eb.from == "" { - eb.from = config.EmailAddress + eb.from = config.Secrets.EmailAddress } if eb.to == "" { diff --git a/modules/email/email.go b/modules/email/email.go index 24d6e3d3..d57e79f2 100644 --- a/modules/email/email.go +++ b/modules/email/email.go @@ -40,5 +40,5 @@ func Send(tmpl, address, title string, args any) error { SetSubject(title). AddPart(*NewPart().SetBody(text.String())). AddPart(*NewPart().SetBody(html.String()).HTML()). - SendEmail(config.IMAPServer) + SendEmail(config.Secrets.EmailServer) } diff --git a/modules/images/images.go b/modules/images/images.go index 28b55b9f..e0bc7592 100644 --- a/modules/images/images.go +++ b/modules/images/images.go @@ -69,13 +69,13 @@ func fixRawURL(url string) string { } func processImages(id, version, url string, data []byte) error { - original := path.Join(config.ImageDir, id+".original") + original := path.Join(config.Storage.ImageDir, id+".original") if err := os.WriteFile(original, data, 0o600); err != nil { return fmt.Errorf("failed to save image for %s from %q: %s", id, url, err) } - dir := path.Join(config.PublicDir, id) + dir := path.Join(config.Storage.PublicDir, id) if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0o755); err != nil { log.Warn.Printf("Failed to create %q for %s: %s\n", dir, id, err) diff --git a/modules/log/log.go b/modules/log/log.go index 4fb80a94..2d67ec1f 100644 --- a/modules/log/log.go +++ b/modules/log/log.go @@ -17,7 +17,7 @@ var ( ) func setOutput(f *os.File) io.Writer { - if config.Production { + if config.App.Production { return io.MultiWriter(f) } @@ -26,9 +26,9 @@ func setOutput(f *os.File) io.Writer { func Initialize() { flags := os.O_APPEND | os.O_CREATE | os.O_WRONLY - f, err := os.OpenFile(config.LogFile, flags, 0o666) + f, err := os.OpenFile(config.Storage.LogFile, flags, 0o666) if err != nil { - log.Fatalf("Failed to open %v: %s\n", config.LogFile, err) + log.Fatalf("Failed to open %v: %s\n", config.Storage.LogFile, err) } // Configure output. diff --git a/modules/oauthlogin/codeberg.go b/modules/oauthlogin/codeberg.go index a8e8e69a..f62a602e 100644 --- a/modules/oauthlogin/codeberg.go +++ b/modules/oauthlogin/codeberg.go @@ -20,7 +20,7 @@ func (codeberg) oauthMakeURL() string { // Base URL. oauthURL := "https://codeberg.org/login/oauth/authorize" // Add our app client ID. - oauthURL += "?client_id=" + config.CodebergClientID + oauthURL += "?client_id=" + config.OpenAuth.CodebergID // Define we want a code back oauthURL += "&response_type=code" @@ -45,11 +45,11 @@ func (codeberg) isAuthTokenPost() bool { func (codeberg) getAuthTokenPostBody(data any) authURLPostBody { return authURLPostBody{ - ClientID: config.CodebergClientID, - ClientSecret: config.CodebergClientSecret, + ClientID: config.OpenAuth.CodebergID, + ClientSecret: config.OpenAuth.CodebergSecret, Code: data.(string), GrantType: "authorization_code", - RedirectURI: config.OAuthURL() + "codeberg/", + RedirectURI: config.App.BaseURL + "/api/callback/codeberg/", } } diff --git a/modules/oauthlogin/github.go b/modules/oauthlogin/github.go index 4f4b8422..6402715e 100644 --- a/modules/oauthlogin/github.go +++ b/modules/oauthlogin/github.go @@ -16,7 +16,7 @@ func (github) oauthMakeURL() string { // Base URL. oauthURL := "https://github.com/login/oauth/authorize" // Add our app client ID. - oauthURL += "?client_id=" + config.GitHubClientID + oauthURL += "?client_id=" + config.OpenAuth.GitHubID // Add email scope. oauthURL += "&scope=" + url.QueryEscape("user:email") @@ -32,13 +32,13 @@ func (github) appendToRedirect(state any) string { // Nonsense state so we later can re-use by decrypting it. // And than have the actual value. Also we use this to specify // From which site the callback was from. - return util.EncryptText(state.(string), util.AEADOAuth, config.ScrambleConfig) + "/" + return util.EncryptText(state.(string), util.AEADOAuth, config.Secrets) + "/" } func (github) getAuthTokenURL(state any) string { authURL := "https://github.com/login/oauth/access_token" - authURL += "?client_id=" + config.GitHubClientID - authURL += "&client_secret=" + config.GitHubClientSecret + authURL += "?client_id=" + config.OpenAuth.GitHubID + authURL += "&client_secret=" + config.OpenAuth.GitHubSecret // Add the nonsense state we uses earlier. authURL += "&state=" + state.(string) diff --git a/modules/oauthlogin/gitlab.go b/modules/oauthlogin/gitlab.go index 3c50c71c..42b10fff 100644 --- a/modules/oauthlogin/gitlab.go +++ b/modules/oauthlogin/gitlab.go @@ -16,7 +16,7 @@ const gitlabStr = "gitlab" func (gitlab) oauthMakeURL() string { oauthURL := "https://gitlab.com/oauth/authorize" // Add our app client ID. - oauthURL += "?client_id=" + config.GitlabClientID + oauthURL += "?client_id=" + config.OpenAuth.GitLabID // Define we want a code back oauthURL += "&response_type=code" // Add read_user scope. @@ -34,12 +34,12 @@ func (gitlab) appendToRedirect(any) string { func (gitlab) getAuthTokenURL(any) string { authURL := "https://gitlab.com/oauth/token" - authURL += "?client_id=" + config.GitlabClientID - authURL += "&client_secret=" + config.GitlabClientSecret + authURL += "?client_id=" + config.OpenAuth.GitLabID + authURL += "&client_secret=" + config.OpenAuth.GitLabSecret // Define we log in trough the temp code. authURL += "&grant_type=authorization_code" // Specify the the redirect uri, because it is required - authURL += "&redirect_uri=" + url.PathEscape(config.OAuthURL()+gitlabStr+"/") + authURL += "&redirect_uri=" + url.PathEscape(config.App.BaseURL+"/api/callback/"+gitlabStr+"/") return authURL } diff --git a/modules/oauthlogin/oauth.go b/modules/oauthlogin/oauth.go index 9249eb5b..edcc0b90 100644 --- a/modules/oauthlogin/oauth.go +++ b/modules/oauthlogin/oauth.go @@ -149,7 +149,7 @@ func OauthMakeURL(serviceType string) string { return "" } - oauthURL += "&redirect_uri=" + config.OAuthURL() + service.appendToRedirect(state) + oauthURL += "&redirect_uri=" + config.App.BaseURL + "/api/callback/" + service.appendToRedirect(state) return oauthURL } diff --git a/modules/storage/style_api_test.go b/modules/storage/style_api_test.go index 3417ad28..c84200d9 100644 --- a/modules/storage/style_api_test.go +++ b/modules/storage/style_api_test.go @@ -96,7 +96,7 @@ func BenchmarkGetStyleCompactIndex(b *testing.B) { UpdatedAt: time.Date(1970, 1, 1, 1, 0, 0, 0, time.UTC), }, Name: "test " + id, - Preview: config.BaseURL + "/preview/" + id + "/0.webp", + Preview: config.App.BaseURL + "/preview/" + id + "/0.webp", }) } diff --git a/modules/storage/style_search.go b/modules/storage/style_search.go index d46442cf..984ed3d9 100644 --- a/modules/storage/style_search.go +++ b/modules/storage/style_search.go @@ -61,10 +61,10 @@ MATCH ?`) b.WriteString(sort) } b.WriteString(" LIMIT ") - b.WriteString(strconv.Itoa(config.AppPageMaxItems)) + b.WriteString(strconv.Itoa(config.App.PageMaxItems)) if page > 1 { b.WriteString(" OFFSET ") - b.WriteString(strconv.Itoa((page - 1) * config.AppPageMaxItems)) + b.WriteString(strconv.Itoa((page - 1) * config.App.PageMaxItems)) } var s []*StyleCard diff --git a/modules/templates/templates.go b/modules/templates/templates.go index b715e580..f0ab16b7 100644 --- a/modules/templates/templates.go +++ b/modules/templates/templates.go @@ -17,17 +17,6 @@ import ( "userstyles.world/modules/util" ) -var appConfig = map[string]string{ - "copyright": time.Now().Format("2006"), - "appName": config.AppName, - "appCodeName": config.AppCodeName, - "appVersion": config.GitVersion, - "appSourceCode": config.AppSourceCode, - "appLatestCommit": config.AppLatestCommit, - "appCommitSHA": config.AppCommitSHA, - "allowedEmailsRe": config.AllowedEmailsRe, -} - type sys struct { Uptime string GoRoutines int @@ -39,7 +28,7 @@ type sys struct { func status() sys { m := new(runtime.MemStats) runtime.ReadMemStats(m) - uptime := time.Since(config.AppUptime).Round(time.Second) + uptime := time.Since(config.App.Started).Round(time.Second) return sys{ Uptime: uptime.String(), @@ -53,10 +42,6 @@ func status() sys { func New(views http.FileSystem) *html.Engine { engine := html.NewFileSystem(views, ".tmpl") - engine.AddFunc("config", func(key string) string { - return appConfig[key] - }) - engine.AddFunc("sys", status) engine.AddFunc("comma", humanize.Comma) @@ -133,9 +118,9 @@ func New(views http.FileSystem) *html.Engine { engine.AddFunc("canonical", func(url any) template.HTML { if url == nil { - return template.HTML(config.BaseURL) + return template.HTML(config.App.BaseURL) } - return template.HTML(config.BaseURL + "/" + url.(string)) + return template.HTML(config.App.BaseURL + "/" + url.(string)) }) engine.AddFunc("Elapsed", func(dur time.Duration) template.HTML { @@ -160,7 +145,7 @@ func New(views http.FileSystem) *html.Engine { return string(b) }) - if !config.Production { + if !config.App.Production { engine.Reload(true) } diff --git a/modules/util/bcrypt.go b/modules/util/bcrypt.go index 582c9b0d..02484592 100644 --- a/modules/util/bcrypt.go +++ b/modules/util/bcrypt.go @@ -8,7 +8,7 @@ import ( // HashPassword generates a hash out of a password. func HashPassword(pw string) (string, error) { - hash, err := bcrypt.GenerateFromPassword([]byte(pw), config.Salt) + hash, err := bcrypt.GenerateFromPassword([]byte(pw), config.Secrets.PasswordCost) if err != nil { return "", err } diff --git a/modules/util/chacha20poly1305.go b/modules/util/chacha20poly1305.go index 0da977b2..b64fd8d3 100644 --- a/modules/util/chacha20poly1305.go +++ b/modules/util/chacha20poly1305.go @@ -15,35 +15,38 @@ var ( AEADCrypto cipher.AEAD AEADOAuth cipher.AEAD AEADOAuthp cipher.AEAD - VerifySigningKey = []byte(config.VerifyJWTSigningKey) - OAuthPSigningKey = []byte(config.OAuthpJWTSigningKey) + VerifySigningKey []byte + OAuthPSigningKey []byte signingMethod = "HS512" ) // InitCrypto initializes cryptographic ciphers. func InitCrypto() { + VerifySigningKey = []byte(config.Secrets.RecoverTokenKey) + OAuthPSigningKey = []byte(config.Secrets.ProviderTokenKey) + var err error - AEADCrypto, err = chacha20poly1305.NewX([]byte(config.CryptoKey)) + AEADCrypto, err = chacha20poly1305.NewX([]byte(config.Secrets.CryptoKey)) if err != nil { log.Warn.Fatalf("Cannot create AEAD_CRYPTO cipher: %s\n", err) } - AEADOAuth, err = chacha20poly1305.NewX([]byte(config.OAuthKey)) + AEADOAuth, err = chacha20poly1305.NewX([]byte(config.Secrets.OAuthClientKey)) if err != nil { log.Warn.Fatalf("Cannot create AEAD_OAUTH cipher: %s\n", err) } - AEADOAuthp, err = chacha20poly1305.NewX([]byte(config.OAuthKey)) + AEADOAuthp, err = chacha20poly1305.NewX([]byte(config.Secrets.OAuthProviderKey)) if err != nil { log.Warn.Fatalf("Cannot create AEAD_OAUTHP cipher: %s\n", err) } } -func sealText(text string, aead cipher.AEAD, nonceScrambling *config.ScrambleSettings) []byte { +func sealText(text string, aead cipher.AEAD, settings *config.SecretsConfig) []byte { nonce := RandomBytes(aead.NonceSize()) dest := aead.Seal(nil, nonce, UnsafeBytes(text), nil) - return scrambleNonce(nonce, dest, nonceScrambling.StepSize, nonceScrambling.BytesPerInsert) + return scrambleNonce(nonce, dest, settings.ScrambleStepSize, settings.ScrambleBytesPerInsert) } // scrambleNonce into string takes a nonce and a text @@ -173,14 +176,14 @@ mainLoop: return nonce, text, nil } -func openText(encryptedMsg string, aead cipher.AEAD, nonceScrambling *config.ScrambleSettings) ([]byte, error) { +func openText(encryptedMsg string, aead cipher.AEAD, settings *config.SecretsConfig) ([]byte, error) { if len(encryptedMsg) < aead.NonceSize() { return nil, errors.ErrMessageSmall } // Split nonce and ciphertext. nonce, ciphertext, err := descrambleNonce(UnsafeBytes(encryptedMsg), aead.NonceSize(), - nonceScrambling.StepSize, nonceScrambling.BytesPerInsert) + settings.ScrambleStepSize, settings.ScrambleBytesPerInsert) if err != nil { return nil, err } @@ -202,7 +205,7 @@ func OAuthPJwtKeyFunction(t *jwt.Token) (any, error) { return OAuthPSigningKey, nil } -func EncryptText(text string, aead cipher.AEAD, settings *config.ScrambleSettings) string { +func EncryptText(text string, aead cipher.AEAD, settings *config.SecretsConfig) string { // We have to prepare the encrypted text for transport // Seal Text -> Base64(URL Version) sealedText := sealText(text, aead, settings) @@ -210,7 +213,7 @@ func EncryptText(text string, aead cipher.AEAD, settings *config.ScrambleSetting return EncodeToString(sealedText) } -func DecryptText(preparedText string, aead cipher.AEAD, settings *config.ScrambleSettings) (string, error) { +func DecryptText(preparedText string, aead cipher.AEAD, settings *config.SecretsConfig) (string, error) { // Now we have to reverse the process. // Decode Base64(URL version) -> Unseal Text enryptedText, err := decodeBase64(preparedText) diff --git a/modules/util/chacha20poly1305_test.go b/modules/util/chacha20poly1305_test.go index 634587d1..4af5ca8f 100644 --- a/modules/util/chacha20poly1305_test.go +++ b/modules/util/chacha20poly1305_test.go @@ -21,9 +21,9 @@ func TestSimpleKey(t *testing.T) { t.Error(err) } - scrambleConfig := &config.ScrambleSettings{ - StepSize: 3, - BytesPerInsert: 2, + scrambleConfig := &config.SecretsConfig{ + ScrambleStepSize: 3, + ScrambleBytesPerInsert: 2, } sealedText := sealText(jwtToken, AEADCrypto, scrambleConfig) @@ -41,7 +41,7 @@ func TestSimpleKey(t *testing.T) { } } -func benchamarkChaCha20Poly1305Seal(b *testing.B, buf []byte, scrambleConfig *config.ScrambleSettings) { +func benchamarkChaCha20Poly1305Seal(b *testing.B, buf []byte, scrambleConfig *config.SecretsConfig) { b.Helper() b.ReportAllocs() @@ -53,7 +53,7 @@ func benchamarkChaCha20Poly1305Seal(b *testing.B, buf []byte, scrambleConfig *co } } -func benchamarkChaCha20Poly1305Open(b *testing.B, buf []byte, scrambleConfig *config.ScrambleSettings) { +func benchamarkChaCha20Poly1305Open(b *testing.B, buf []byte, scrambleConfig *config.SecretsConfig) { b.Helper() b.ReportAllocs() @@ -67,7 +67,7 @@ func benchamarkChaCha20Poly1305Open(b *testing.B, buf []byte, scrambleConfig *co } } -func benchamarkPrepareText(b *testing.B, buf []byte, scrambleConfig *config.ScrambleSettings) { +func benchamarkPrepareText(b *testing.B, buf []byte, scrambleConfig *config.SecretsConfig) { b.Helper() b.ReportAllocs() @@ -79,7 +79,7 @@ func benchamarkPrepareText(b *testing.B, buf []byte, scrambleConfig *config.Scra } } -func benchamarkDecodePreparedText(b *testing.B, buf []byte, scrambleConfig *config.ScrambleSettings) { +func benchamarkDecodePreparedText(b *testing.B, buf []byte, scrambleConfig *config.SecretsConfig) { b.Helper() b.ReportAllocs() @@ -97,9 +97,9 @@ func BenchmarkPureChaCha20Poly1305(b *testing.B) { InitCrypto() b.ResetTimer() - scrambleConfig := &config.ScrambleSettings{ - StepSize: 2, - BytesPerInsert: 4, + scrambleConfig := &config.SecretsConfig{ + ScrambleStepSize: 2, + ScrambleBytesPerInsert: 4, } for _, length := range []int{215, 1350, 8 * 1024} { @@ -116,9 +116,9 @@ func BenchmarkPrepareText(b *testing.B) { InitCrypto() b.ResetTimer() - scrambleConfig := &config.ScrambleSettings{ - StepSize: 2, - BytesPerInsert: 4, + scrambleConfig := &config.SecretsConfig{ + ScrambleStepSize: 2, + ScrambleBytesPerInsert: 4, } for _, length := range []int{215, 1350, 8 * 1024} { diff --git a/modules/util/crypto.go b/modules/util/crypto.go index ab80fb1e..ae245777 100644 --- a/modules/util/crypto.go +++ b/modules/util/crypto.go @@ -20,7 +20,7 @@ var ( } hmacPool = sync.Pool{ New: func() interface{} { - return hmac.New(sha512.New, []byte(config.StatsKey)) + return hmac.New(sha512.New, []byte(config.Secrets.StatsKey)) }, } ) diff --git a/modules/util/jwt.go b/modules/util/jwt.go index 9cd8bb50..e2bb5b62 100644 --- a/modules/util/jwt.go +++ b/modules/util/jwt.go @@ -30,7 +30,7 @@ func (jt *JWTTokenBuilder) SetExpiration(duration time.Time) *JWTTokenBuilder { func (jt *JWTTokenBuilder) GetSignedString(customKey []byte) (string, error) { if customKey == nil { - customKey = []byte(config.JWTSigningKey) + customKey = []byte(config.Secrets.SessionTokenKey) } return jt.SignedString(customKey) } diff --git a/tools/run b/tools/run index 9d887809..43937c23 100755 --- a/tools/run +++ b/tools/run @@ -42,6 +42,15 @@ check() { # Jobs. build() { + : "${flags:=""}" + : "${config:="userstyles.world/modules/config"}" + + setBuildInfo() { + flags="$flags -X ${config}.GoVersion=$(go version | cut -d ' ' -f 3 | cut -c 3-)" + flags="$flags -X ${config}.GitCommit=$(git rev-list -1 HEAD)" + flags="$flags -X ${config}.GitSignature=$(git describe --tags --dirty)" + } + case "$1" in fonts) log "Downloading fonts." @@ -50,21 +59,23 @@ build() { go-dev) : "Setting BIN to ${BIN:=bin/userstyles-dev}." log "Compiling development executable to ${BIN}." - go build -v -o "$BIN" -tags "fts5" cmd/userstyles-world/main.go + + setBuildInfo + go build -v \ + -o "$BIN" \ + -ldflags "$flags" \ + -tags "fts5" \ + cmd/userstyles-world/main.go ;; go-prod) : "Setting BIN to ${BIN:=bin/userstyles-prod}." log "Compiling production executable to ${BIN}." - c="$(git rev-list -1 HEAD)" - v="$(git describe --tags)" - f="-s -w -extldflags '-fno-PIC -static'" - f="$f -X userstyles.world/modules/config.GitCommit=${c}" - f="$f -X userstyles.world/modules/config.GitVersion=${v}" - + flags="-s -w -extldflags '-fno-PIC -static'" + setBuildInfo go build -v \ -o "$BIN" \ - -ldflags "$f" \ + -ldflags "$flags" \ -buildmode pie \ -tags 'osusergo netgo static_build fts5' \ cmd/userstyles-world/main.go @@ -151,7 +162,7 @@ setup() { if [ "$BUILD" -eq 1 ]; then build sass-dev; fi ;; all) - setup fonts; setup ts; setup sass + setup fonts; setup ts; setup sass; setup go ;; esac } diff --git a/web/views/core/home.tmpl b/web/views/core/home.tmpl index a61a2ccb..49fa19c1 100644 --- a/web/views/core/home.tmpl +++ b/web/views/core/home.tmpl @@ -1,7 +1,7 @@ {{ if not .User.ID }}
{{ template "partials/mascot" }}
-

{{ config "appName" }}

+

{{ .App.Name }}

Free and open-source, community-driven platform for sharing and browsing UserCSS userstyles, and a replacement for UserStyles.org, made by the userstyles community.

diff --git a/web/views/partials/footer.tmpl b/web/views/partials/footer.tmpl index 5223377f..390af80e 100644 --- a/web/views/partials/footer.tmpl +++ b/web/views/partials/footer.tmpl @@ -2,19 +2,9 @@