package cmds import ( "context" "html" "html/template" "log" "path/filepath" "strings" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" "github.com/gofiber/fiber/v3/middleware/session" "github.com/gofiber/fiber/v3/middleware/static" "github.com/gofiber/storage/sqlite3/v2" fiber_html "github.com/gofiber/template/html/v3" "github.com/gofiber/utils/v2" "github.com/spf13/cobra" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "lmika.dev/lmika/weiro/config" "lmika.dev/lmika/weiro/handlers" "lmika.dev/lmika/weiro/handlers/middleware" "lmika.dev/lmika/weiro/services" ) func Root() *cobra.Command { cmd := &cobra.Command{ Use: "weiro", Short: "Weiro is a simple blogging platform", Long: `Weiro is a simple blogging platform. Starting weiro without any arguments will start the server. `, Run: func(cmd *cobra.Command, args []string) { cfg, err := config.LoadConfig() if err != nil { log.Fatal(err) } svcs, err := services.New(cfg) if err != nil { log.Fatal(err) } defer svcs.Close() svcs.PublisherQueue.Start(context.Background()) fiberTemplate := fiber_html.New("./views", ".html") fiberTemplate.Funcmap["sub"] = func(x, y int) int { return x - y } fiberTemplate.Funcmap["markdown"] = func() func(s string) template.HTML { mdParser := goldmark.New( goldmark.WithExtensions(extension.GFM), ) return func(s string) template.HTML { var sb strings.Builder if err := mdParser.Convert([]byte(s), &sb); err != nil { return template.HTML("Markdown error: " + html.EscapeString(err.Error())) } return template.HTML(sb.String()) } }() // Initialize custom config store := sqlite3.New(sqlite3.Config{ Database: filepath.Join(cfg.DataDir, "./fiber.db"), Table: "fiber_storage", Reset: false, GCInterval: 10 * time.Second, MaxOpenConns: 100, MaxIdleConns: 100, ConnMaxLifetime: 1 * time.Second, }) app := fiber.New(fiber.Config{ Views: fiberTemplate, ViewsLayout: "layouts/main", PassLocalsToViews: true, }) app.Use(session.New(session.Config{ // Storage Storage: store, // Security CookieSecure: cfg.IsProd(), CookieSameSite: "Lax", // Session Management IdleTimeout: 24 * time.Hour, // Inactivity timeout AbsoluteTimeout: 7 * 24 * time.Hour, // Maximum session duration // Cookie Settings CookiePath: "/", CookieDomain: cfg.SiteDomain, CookieSessionOnly: false, // Persist across browser restarts // Session ID Extractor: extractors.FromCookie("__wro-session_id"), KeyGenerator: utils.SecureToken, // Error Handling ErrorHandler: func(c fiber.Ctx, err error) { log.Printf("Session error: %v", err) }, })) ih := handlers.IndexHandler{SiteService: svcs.Sites} lh := handlers.LoginHandler{Config: cfg, AuthService: svcs.Auth} ph := handlers.PostsHandler{PostService: svcs.Posts} uh := handlers.UploadsHandler{UploadsService: svcs.Uploads} app.Get("/login", lh.Login) app.Post("/login", lh.DoLogin) app.Post("/logout", lh.Logout) siteGroup := app.Group("/sites/:siteID", middleware.RequireUser(svcs.Auth), middleware.RequiresSite(svcs.Sites)) siteGroup.Get("/posts", ph.Index) siteGroup.Get("/posts/new", ph.New) siteGroup.Get("/posts/:postID", ph.Edit) siteGroup.Post("/posts", ph.Update) siteGroup.Patch("/posts/:postID", ph.Patch) siteGroup.Delete("/posts/:postID", ph.Delete) siteGroup.Get("/uploads", uh.Index) siteGroup.Get("/uploads/:uploadID", uh.Show) siteGroup.Get("/uploads/:uploadID/raw", uh.ShowRaw) siteGroup.Post("/uploads/pending", uh.New) siteGroup.Post("/uploads/pending/:guid", uh.UploadPart) siteGroup.Post("/uploads/pending/:guid/finalize", uh.UploadComplete) app.Get("/", middleware.OptionalUser(svcs.Auth), ih.Index) app.Get("/first-run", ih.FirstRun) app.Post("/first-run", ih.FirstRunSubmit) app.Get("/static/*", static.New("./static")) if err := app.Listen(":3000"); err != nil { log.Println(err) } }, } cmd.AddCommand(Sites()) cmd.AddCommand(PubTargets()) return cmd }