diff options
Diffstat (limited to 'app/cmd')
-rw-r--r-- | app/cmd/migrate.go | 51 | ||||
-rw-r--r-- | app/cmd/root.go | 52 | ||||
-rw-r--r-- | app/cmd/serve.go | 134 |
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 +} |