aboutsummaryrefslogtreecommitdiff
path: root/app/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'app/cmd')
-rw-r--r--app/cmd/migrate.go51
-rw-r--r--app/cmd/root.go52
-rw-r--r--app/cmd/serve.go134
3 files changed, 237 insertions, 0 deletions
diff --git a/app/cmd/migrate.go b/app/cmd/migrate.go
new file mode 100644
index 0000000..3c4fd05
--- /dev/null
+++ b/app/cmd/migrate.go
@@ -0,0 +1,51 @@
+package cmd
+
+import (
+ "database/sql"
+ "fmt"
+
+ "github.com/golang-migrate/migrate/v4"
+ "github.com/golang-migrate/migrate/v4/database/sqlite"
+ "github.com/golang-migrate/migrate/v4/source/iofs"
+ "github.com/spf13/cobra"
+ "gitrepo.ru/neonxp/gorum/migrations"
+)
+
+var migrateCmd = &cobra.Command{
+ Use: "migrate",
+ Short: "Migrate db",
+ Long: `Up and down migrations`,
+}
+
+var migrateUp = &cobra.Command{
+ Use: "up",
+ Short: "Migrate up",
+ Long: `Up migrations`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ db, err := sql.Open("sqlite3", dbFile)
+ if err != nil {
+ return fmt.Errorf("open db failed: %w", err)
+ }
+ defer db.Close()
+
+ driver, err := sqlite.WithInstance(db, &sqlite.Config{})
+ if err != nil {
+ return fmt.Errorf("failed create migration driver: %w", err)
+ }
+ sourceDriver, err := iofs.New(migrations.FS, ".")
+ if err != nil {
+ return fmt.Errorf("failed open migrations: %w", err)
+ }
+
+ m, err := migrate.NewWithInstance("fs", sourceDriver, "sqlite3", driver)
+ if err != nil {
+ return fmt.Errorf("open migration failed: %w", err)
+ }
+
+ if err := m.Up(); err != nil && err != migrate.ErrNoChange {
+ return fmt.Errorf("do migration failed: %w", err)
+ }
+
+ return nil
+ },
+}
diff --git a/app/cmd/root.go b/app/cmd/root.go
new file mode 100644
index 0000000..33c3642
--- /dev/null
+++ b/app/cmd/root.go
@@ -0,0 +1,52 @@
+package cmd
+
+import (
+ "fmt"
+ "log/slog"
+ "os"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var (
+ cfgFile string
+ dbFile string
+ debug bool
+ rootCmd = &cobra.Command{Use: "gorum"}
+)
+
+func init() {
+ cobra.OnInitialize(initLogger)
+ cobra.OnInitialize(initConfig)
+ rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "gorum.yaml", "config file (default is 'gorum.yaml')")
+ rootCmd.PersistentFlags().StringVar(&dbFile, "db", "gorum.db", "database file (default is 'gorum.db')")
+ rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "verbose debug output")
+ viper.BindPFlag("db", serverCmd.Flags().Lookup("db"))
+}
+
+func initLogger() {
+ level := slog.LevelInfo
+ if debug {
+ level = slog.LevelDebug
+ }
+ logger := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level})
+ slog.SetDefault(slog.New(logger))
+}
+
+func initConfig() {
+ viper.SetConfigFile(cfgFile)
+
+ if err := viper.ReadInConfig(); err != nil {
+ slog.Warn("can't read config", slog.Any("error", err))
+ }
+}
+
+func Execute() {
+ rootCmd.AddCommand(serverCmd)
+
+ if err := rootCmd.Execute(); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+}
diff --git a/app/cmd/serve.go b/app/cmd/serve.go
new file mode 100644
index 0000000..5593c32
--- /dev/null
+++ b/app/cmd/serve.go
@@ -0,0 +1,134 @@
+package cmd
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "log/slog"
+ "net"
+ "net/http"
+
+ "github.com/labstack/echo-contrib/session"
+ "github.com/labstack/echo/v4"
+ echomiddleware "github.com/labstack/echo/v4/middleware"
+ _ "github.com/mattn/go-sqlite3"
+ "github.com/michaeljs1990/sqlitestore"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/uptrace/bun"
+ "github.com/uptrace/bun/dialect/sqlitedialect"
+ "github.com/uptrace/bun/extra/bundebug"
+ "gitrepo.ru/neonxp/gorum/contextlib"
+ "gitrepo.ru/neonxp/gorum/middleware"
+ "gitrepo.ru/neonxp/gorum/repository"
+ "gitrepo.ru/neonxp/gorum/routes"
+ "gitrepo.ru/neonxp/gorum/utils"
+ "gitrepo.ru/neonxp/gorum/views"
+ "gitrepo.ru/neonxp/gorum/views/assets"
+)
+
+var (
+ theme string
+ listen string
+ sessionSecret string
+ serverCmd = &cobra.Command{
+ Use: "serve",
+ Short: "Run server",
+ Long: `Run forum server`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return serve(cmd.Context())
+ },
+ }
+)
+
+func init() {
+ serverCmd.PersistentFlags().StringVar(&theme, "theme", "default", "theme to use (default is 'default')")
+ serverCmd.PersistentFlags().StringVar(&listen, "listen", ":8000", "bind address to listen (default is ':8000')")
+ serverCmd.PersistentFlags().StringVar(&sessionSecret, "session_secret", "s3cr3t", "sessions secret (default is 's3cr3t')")
+ viper.BindPFlag("theme", serverCmd.Flags().Lookup("theme"))
+ viper.BindPFlag("listen", serverCmd.Flags().Lookup("listen"))
+ viper.BindPFlag("session_secret", serverCmd.Flags().Lookup("session_secret"))
+}
+
+func serve(ctx context.Context) error {
+
+ slog.Debug(
+ "params",
+ slog.String("listen", listen),
+ slog.String("theme", theme),
+ slog.String("session_secret", sessionSecret),
+ )
+ ctx = context.WithValue(ctx, contextlib.ThemeKey, theme)
+
+ db, err := sql.Open("sqlite3", dbFile)
+ if err != nil {
+ return fmt.Errorf("open db failed: %w", err)
+ }
+ defer db.Close()
+
+ orm := bun.NewDB(db, sqlitedialect.New())
+ orm.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
+
+ userRepo := repository.NewUser(orm)
+ nodeRepo := repository.NewNode(orm)
+
+ r := routes.NewRouter(userRepo, nodeRepo)
+
+ e := echo.New()
+
+ e.HideBanner = true
+
+ e.HTTPErrorHandler = func(err error, c echo.Context) {
+ _ = utils.Render(c, views.ErrorPage(err))
+ }
+
+ e.Server.BaseContext = func(l net.Listener) context.Context {
+ return ctx
+ }
+
+ sessionStore, err := sqlitestore.NewSqliteStoreFromConnection(db, "sessions", "", 0, []byte(sessionSecret))
+ if err != nil {
+ return fmt.Errorf("failed init session store: %w", err)
+ }
+
+ e.Use(
+ echomiddleware.Recover(),
+ echomiddleware.Gzip(),
+ echomiddleware.CSRFWithConfig(echomiddleware.CSRFConfig{
+ Skipper: echomiddleware.DefaultSkipper,
+ TokenLength: 32,
+ TokenLookup: "form:" + echo.HeaderXCSRFToken,
+ ContextKey: "csrf",
+ CookieName: "_csrf",
+ CookieMaxAge: 86400,
+ }),
+ session.Middleware(sessionStore),
+ middleware.UserMiddleware(),
+ )
+
+ e.GET("/register", r.Register)
+ e.POST("/register", r.Register)
+ e.GET("/login", r.Login)
+ e.POST("/login", r.Login)
+ e.POST("/logout", r.Logout)
+
+ e.GET("/", r.Node)
+ e.GET("/n/:id", r.Node)
+ e.GET("/n/:id/new", r.NewPost)
+ e.POST("/n/:id/new", r.NewPost)
+
+ e.StaticFS("/assets", assets.FS)
+
+ slog.InfoContext(ctx, "started gorum", slog.String("bind", listen))
+
+ server := http.Server{
+ Addr: listen,
+ Handler: e,
+ ErrorLog: slog.NewLogLogger(slog.Default().Handler(), slog.LevelError),
+ }
+ if err := server.ListenAndServe(); err != http.ErrServerClosed {
+ return fmt.Errorf("server failed: %w", err)
+ }
+
+ return nil
+}