From ce3111b0efe91e275ce070f9511b5b1b9801a46d Mon Sep 17 00:00:00 2001 From: Alexander NeonXP Kiryukhin Date: Sun, 21 Jul 2024 19:26:56 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=BD=D0=BE=D0=B6=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=BE=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .goreleaser.yaml | 1 + app/cmd/migrate.go | 110 ++++++++++---- app/cmd/root.go | 10 +- app/cmd/serve.go | 53 ++++--- app/cmd/user.go | 80 +++++++++++ db/db.go | 34 +++++ go.mod | 2 - go.sum | 26 ++-- middleware/session/store.go | 30 ++++ models/node.go | 1 + models/user.go | 9 ++ repository/node.go | 10 +- repository/user.go | 9 +- routes/node.go | 50 ++++++- routes/user.go | 3 +- views/csrf.templ | 3 +- views/csrf_templ.go | 31 ++++ views/error.templ | 8 +- views/error_templ.go | 6 +- views/error_templ.txt | 1 - views/layouts.templ | 27 +++- views/layouts_templ.go | 86 ++++++++--- views/layouts_templ.txt | 7 - views/login.templ | 2 +- views/login_templ.go | 6 +- views/login_templ.txt | 2 - views/new-node.templ | 30 ---- views/new-node_templ.go | 85 ----------- views/new-node_templ.txt | 5 - views/new.templ | 51 +++++++ views/new_templ.go | 271 +++++++++++++++++++++++++++++++++++ views/nodes.templ | 93 +++++++----- views/nodes_templ.go | 276 ++++++++++++++++++++++++------------ views/nodes_templ.txt | 18 --- views/register.templ | 2 +- views/register_templ.go | 8 +- views/register_templ.txt | 3 - 37 files changed, 1047 insertions(+), 402 deletions(-) create mode 100644 app/cmd/user.go create mode 100644 db/db.go create mode 100644 middleware/session/store.go create mode 100644 views/csrf_templ.go delete mode 100644 views/error_templ.txt delete mode 100644 views/layouts_templ.txt delete mode 100644 views/login_templ.txt delete mode 100644 views/new-node.templ delete mode 100644 views/new-node_templ.go delete mode 100644 views/new-node_templ.txt create mode 100644 views/new.templ create mode 100644 views/new_templ.go delete mode 100644 views/nodes_templ.txt delete mode 100644 views/register_templ.txt diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 0ec7c5e..525ddf8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -12,6 +12,7 @@ before: hooks: - go mod tidy - go generate ./... + - templ generate builds: - env: diff --git a/app/cmd/migrate.go b/app/cmd/migrate.go index 3c4fd05..95a1dc3 100644 --- a/app/cmd/migrate.go +++ b/app/cmd/migrate.go @@ -17,35 +17,83 @@ var migrateCmd = &cobra.Command{ 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 - }, +func init() { + migrateCmd.AddCommand( + &cobra.Command{ + Use: "up", + Short: "Migrate up", + Long: `Up migrations`, + RunE: up, + }, + &cobra.Command{ + Use: "down", + Short: "Migrate down", + Long: `Down migrations`, + RunE: down, + }, + &cobra.Command{ + Use: "drop", + Short: "Drop db", + Long: `Deletes everything in the database`, + RunE: drop, + }, + ) +} + +func up(_ *cobra.Command, _ []string) error { + m, err := initMigrate() + if err != nil { + return fmt.Errorf("open migration failed: %w", err) + } + defer m.Close() + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + return fmt.Errorf("do migration failed: %w", err) + } + + return nil +} + +func down(_ *cobra.Command, _ []string) error { + m, err := initMigrate() + if err != nil { + return fmt.Errorf("open migration failed: %w", err) + } + defer m.Close() + if err := m.Down(); err != nil && err != migrate.ErrNoChange { + return fmt.Errorf("do migration failed: %w", err) + } + + return nil +} + +func drop(_ *cobra.Command, _ []string) error { + m, err := initMigrate() + if err != nil { + return fmt.Errorf("open migration failed: %w", err) + } + defer m.Close() + if err := m.Drop(); err != nil && err != migrate.ErrNoChange { + return fmt.Errorf("do migration failed: %w", err) + } + + return nil +} + +func initMigrate() (*migrate.Migrate, error) { + db, err := sql.Open("sqlite3", dbFile) + if err != nil { + return nil, fmt.Errorf("open db failed: %w", err) + } + defer db.Close() + + driver, err := sqlite.WithInstance(db, &sqlite.Config{}) + if err != nil { + return nil, fmt.Errorf("failed create migration driver: %w", err) + } + sourceDriver, err := iofs.New(migrations.FS, ".") + if err != nil { + return nil, fmt.Errorf("failed open migrations: %w", err) + } + + return migrate.NewWithInstance("fs", sourceDriver, "sqlite3", driver) } diff --git a/app/cmd/root.go b/app/cmd/root.go index 33c3642..5803166 100644 --- a/app/cmd/root.go +++ b/app/cmd/root.go @@ -19,10 +19,14 @@ var ( 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().StringVar(&cfgFile, "config", "gorum.yaml", "config file") + rootCmd.PersistentFlags().StringVar(&dbFile, "db", "gorum.db", "database file") rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "verbose debug output") viper.BindPFlag("db", serverCmd.Flags().Lookup("db")) + + rootCmd.AddCommand(serverCmd) + rootCmd.AddCommand(migrateCmd) + rootCmd.AddCommand(userCmd) } func initLogger() { @@ -43,8 +47,6 @@ func initConfig() { } 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 index 5593c32..e2d2843 100644 --- a/app/cmd/serve.go +++ b/app/cmd/serve.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "database/sql" "fmt" "log/slog" "net" @@ -15,10 +14,8 @@ import ( "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/db" "gitrepo.ru/neonxp/gorum/middleware" "gitrepo.ru/neonxp/gorum/repository" "gitrepo.ru/neonxp/gorum/routes" @@ -42,9 +39,9 @@ var ( ) 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')") + serverCmd.PersistentFlags().StringVar(&theme, "theme", "default", "theme to use") + serverCmd.PersistentFlags().StringVar(&listen, "listen", ":8000", "bind address to listen") + serverCmd.PersistentFlags().StringVar(&sessionSecret, "session_secret", "s3cr3t", "sessions secret") viper.BindPFlag("theme", serverCmd.Flags().Lookup("theme")) viper.BindPFlag("listen", serverCmd.Flags().Lookup("listen")) viper.BindPFlag("session_secret", serverCmd.Flags().Lookup("session_secret")) @@ -60,14 +57,12 @@ func serve(ctx context.Context) error { ) ctx = context.WithValue(ctx, contextlib.ThemeKey, theme) - db, err := sql.Open("sqlite3", dbFile) + orm, err := db.GetDB(dbFile) if err != nil { - return fmt.Errorf("open db failed: %w", err) + return err } - defer db.Close() - orm := bun.NewDB(db, sqlitedialect.New()) - orm.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true))) + defer orm.Close() userRepo := repository.NewUser(orm) nodeRepo := repository.NewNode(orm) @@ -82,11 +77,7 @@ func serve(ctx context.Context) error { _ = 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)) + sessionStore, err := sqlitestore.NewSqliteStoreFromConnection(orm.DB, "sessions", "", 0, []byte(sessionSecret)) if err != nil { return fmt.Errorf("failed init session store: %w", err) } @@ -94,14 +85,14 @@ func serve(ctx context.Context) error { 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, - }), + // echomiddleware.CSRFWithConfig(echomiddleware.CSRFConfig{ + // Skipper: echomiddleware.DefaultSkipper, + // TokenLength: 32, + // TokenLookup: "form:" + echo.HeaderXCSRFToken, + // ContextKey: "csrf", + // CookieName: "_csrf", + // CookieMaxAge: 86400, + // }), session.Middleware(sessionStore), middleware.UserMiddleware(), ) @@ -113,9 +104,12 @@ func serve(ctx context.Context) error { 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.GET("/p/:id", r.Node) + e.GET("/p/:id/new", r.NewPost) + e.POST("/p/:id/new", r.NewPost) + e.GET("/t/:id", r.Node) + e.GET("/t/:id/new", r.NewTopic) + e.POST("/t/:id/new", r.NewTopic) e.StaticFS("/assets", assets.FS) @@ -125,6 +119,9 @@ func serve(ctx context.Context) error { Addr: listen, Handler: e, ErrorLog: slog.NewLogLogger(slog.Default().Handler(), slog.LevelError), + ConnContext: func(cctx context.Context, c net.Conn) context.Context { + return ctx + }, } if err := server.ListenAndServe(); err != http.ErrServerClosed { return fmt.Errorf("server failed: %w", err) diff --git a/app/cmd/user.go b/app/cmd/user.go new file mode 100644 index 0000000..673c7b8 --- /dev/null +++ b/app/cmd/user.go @@ -0,0 +1,80 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + + "github.com/spf13/cobra" + "gitrepo.ru/neonxp/gorum/db" + "gitrepo.ru/neonxp/gorum/models" + "gitrepo.ru/neonxp/gorum/repository" +) + +var userCmd = &cobra.Command{ + Use: "user", + Short: "User managment", +} + +var createUserCmd = &cobra.Command{ + Use: "add", + Args: cobra.ExactArgs(3), + ArgAliases: []string{"username", "email", "role"}, + RunE: func(cmd *cobra.Command, args []string) error { + orm, err := db.GetDB(dbFile) + if err != nil { + return fmt.Errorf("failed init db: %w", err) + } + username := args[0] + email := args[1] + role := args[2] + iRole := models.RoleUser + switch role { + case "admin": + iRole = models.RoleAdmin + case "moderator": + iRole = models.RoleModerator + } + reader := bufio.NewReader(os.Stdin) + fmt.Printf("Enter password for user %s: ", username) + password, _ := reader.ReadString('\n') + + ur := repository.NewUser(orm) + id, err := ur.Create(cmd.Context(), email, password, username, iRole) + if err != nil { + return fmt.Errorf("failed create user: %w", err) + } + + fmt.Printf("Created user %s (id=%d, email=%s, role_id=%d)\n", username, id, email, iRole) + + return nil + }, +} + +var listUserCmd = &cobra.Command{ + Use: "list", + RunE: func(cmd *cobra.Command, args []string) error { + orm, err := db.GetDB(dbFile) + if err != nil { + return fmt.Errorf("failed init db: %w", err) + } + + ur := repository.NewUser(orm) + users, err := ur.List(cmd.Context()) + if err != nil { + return err + } + + fmt.Printf("ID\tUsername\tEmail\tRole\n") + for _, u := range users { + fmt.Printf("%d\t%s\t%s\t%d\n", u.ID, u.Username, u.Email, u.Role) + } + + return nil + }, +} + +func init() { + userCmd.AddCommand(createUserCmd) + userCmd.AddCommand(listUserCmd) +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..5241cd1 --- /dev/null +++ b/db/db.go @@ -0,0 +1,34 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + "log/slog" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/sqlitedialect" +) + +func GetDB(dbFile string) (*bun.DB, error) { + db, err := sql.Open("sqlite3", dbFile) + if err != nil { + return nil, fmt.Errorf("open db failed: %w", err) + } + + orm := bun.NewDB(db, sqlitedialect.New()) + orm.AddQueryHook(&queryHook{}) + + return orm, nil +} + +type queryHook struct { +} + +func (*queryHook) BeforeQuery(ctx context.Context, qe *bun.QueryEvent) context.Context { + return ctx +} + +func (*queryHook) AfterQuery(ctx context.Context, qe *bun.QueryEvent) { + slog.Debug("query", slog.String("query", qe.Query)) +} diff --git a/go.mod b/go.mod index 3ce452f..62a24ec 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( ) require ( - github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/google/uuid v1.4.0 // indirect github.com/gorilla/context v1.1.2 // indirect @@ -67,7 +66,6 @@ require ( github.com/spf13/viper v1.19.0 github.com/uptrace/bun v1.2.1 github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 - github.com/uptrace/bun/extra/bundebug v1.2.1 github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/crypto v0.22.0 diff --git a/go.sum b/go.sum index 127edf3..3273127 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,13 @@ github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg= github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= @@ -43,6 +44,10 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo-contrib v0.17.1 h1:7I/he7ylVKsDUieaGRZ9XxxTYOjfQwVzHzUYrNykfCU= github.com/labstack/echo-contrib v0.17.1/go.mod h1:SnsCZtwHBAZm5uBSAtQtXQHI3wqEA73hvTn0bYMKnZA= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= @@ -65,12 +70,17 @@ github.com/michaeljs1990/sqlitestore v0.0.0-20210507162135-8585425bc864 h1:NkqeB github.com/michaeljs1990/sqlitestore v0.0.0-20210507162135-8585425bc864/go.mod h1:N6aiMetO+sSN0h4VC8RjkwiljKaZmgPsWzZG+mk6oec= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= @@ -105,8 +115,6 @@ github.com/uptrace/bun v1.2.1 h1:2ENAcfeCfaY5+2e7z5pXrzFKy3vS8VXvkCag6N2Yzfk= github.com/uptrace/bun v1.2.1/go.mod h1:cNg+pWBUMmJ8rHnETgf65CEvn3aIKErrwOD6IA8e+Ec= github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 h1:IprvkIKUjEjvt4VKpcmLpbMIucjrsmUPJOSlg19+a0Q= github.com/uptrace/bun/dialect/sqlitedialect v1.2.1/go.mod h1:mMQf4NUpgY8bnOanxGmxNiHCdALOggS4cZ3v63a9D/o= -github.com/uptrace/bun/extra/bundebug v1.2.1 h1:85MYpX3QESYI02YerKxUi1CD9mHuLrc2BXs1eOCtQus= -github.com/uptrace/bun/extra/bundebug v1.2.1/go.mod h1:sfGKIi0HSGxsTC/sgIHGwpnYduHHYhdMeOIwurgSY+Y= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -137,8 +145,8 @@ golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -164,6 +172,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/middleware/session/store.go b/middleware/session/store.go new file mode 100644 index 0000000..f22c64d --- /dev/null +++ b/middleware/session/store.go @@ -0,0 +1,30 @@ +package session + +import ( + "net/http" + + "github.com/gorilla/sessions" + "github.com/uptrace/bun" +) + +type SessionStore struct { + orm *bun.DB +} + +// Get should return a cached session. +func (s *SessionStore) Get(r *http.Request, name string) (*sessions.Session, error) { + panic("not implemented") // TODO: Implement +} + +// New should create and return a new session. +// +// Note that New should never return a nil session, even in the case of +// an error if using the Registry infrastructure to cache the session. +func (s *SessionStore) New(r *http.Request, name string) (*sessions.Session, error) { + panic("not implemented") // TODO: Implement +} + +// Save should persist session to the underlying store implementation. +func (s *SessionStore) Save(r *http.Request, w http.ResponseWriter, ss *sessions.Session) error { + panic("not implemented") // TODO: Implement +} diff --git a/models/node.go b/models/node.go index 16e426a..98a8d4e 100644 --- a/models/node.go +++ b/models/node.go @@ -21,6 +21,7 @@ type Node struct { CreatedAt int64 `bun:",nullzero,notnull,default:current_timestamp"` UpdatedAt int64 `bun:",nullzero,notnull,default:current_timestamp"` DeletedAt int64 + Children []*Node `bun:"rel:has-many,join:id=parent_id"` } var _ bun.BeforeAppendModelHook = (*Node)(nil) diff --git a/models/user.go b/models/user.go index 73afdce..73358fe 100644 --- a/models/user.go +++ b/models/user.go @@ -18,4 +18,13 @@ type User struct { Password string Username string Photo *string + Role UserRole } + +type UserRole int + +const ( + RoleUser UserRole = iota + RoleModerator + RoleAdmin +) diff --git a/repository/node.go b/repository/node.go index d5794b6..27282af 100644 --- a/repository/node.go +++ b/repository/node.go @@ -42,17 +42,17 @@ func (t *Node) Get(ctx context.Context, topicID int) (*models.Node, error) { Model(node). Where(`n.id = ?`, topicID). Relation("Author"). + Relation("Parent"). Scan(ctx) } -func (t *Node) List(ctx context.Context, topicID int) ([]*models.Node, int, error) { +func (t *Node) List(ctx context.Context, topicID int) ([]*models.Node, error) { posts := make([]*models.Node, 0) - count, err := t.db.NewSelect(). + return posts, t.db.NewSelect(). Model(&posts). Where(`parent_id = ?`, topicID). Relation("Author"). - ScanAndCount(ctx) - - return posts, count, err + Relation("Children"). + Scan(ctx) } diff --git a/repository/user.go b/repository/user.go index ec3b702..5c3cce6 100644 --- a/repository/user.go +++ b/repository/user.go @@ -19,7 +19,7 @@ func NewUser(db *bun.DB) *User { } } -func (u *User) Create(ctx context.Context, email, password, username string) (int, error) { +func (u *User) Create(ctx context.Context, email, password, username string, role models.UserRole) (int, error) { hpassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { @@ -30,6 +30,7 @@ func (u *User) Create(ctx context.Context, email, password, username string) (in Email: email, Password: string(hpassword), Username: username, + Role: role, } if _, err := u.db.NewInsert().Model(user).Returning("id").Exec(ctx); err != nil { @@ -52,3 +53,9 @@ func (u *User) Login(ctx context.Context, email, password string) (*models.User, return user, nil } + +func (u *User) List(ctx context.Context) ([]*models.User, error) { + ret := make([]*models.User, 0) + + return ret, u.db.NewSelect().Model(&ret).Scan(ctx) +} diff --git a/routes/node.go b/routes/node.go index c754b9a..2f5b7d3 100644 --- a/routes/node.go +++ b/routes/node.go @@ -34,12 +34,23 @@ func (r *Router) Node(c echo.Context) error { } } - nodes, count, err := r.nodeRepo.List(c.Request().Context(), parentID) + nodes, err := r.nodeRepo.List(c.Request().Context(), parentID) if err != nil { return err } - return utils.Render(c, views.Node(node, nodes, count)) + topics := make([]*models.Node, 0, len(nodes)) + posts := make([]*models.Node, 0, len(nodes)) + for _, n := range nodes { + switch n.Type { + case models.PostType: + posts = append(posts, n) + case models.TopicType: + topics = append(topics, n) + } + } + + return utils.Render(c, views.Node(node, topics, posts)) } func (r *Router) NewPost(c echo.Context) error { @@ -63,7 +74,7 @@ func (r *Router) NewPost(c echo.Context) error { return err } - return c.Redirect(302, fmt.Sprintf("/n/%d#post%d", parentID, postID)) + return c.Redirect(302, fmt.Sprintf("/t/%d#post%d", parentID, postID)) } node, err := r.nodeRepo.Get(c.Request().Context(), parentID) @@ -71,5 +82,36 @@ func (r *Router) NewPost(c echo.Context) error { return err } - return utils.Render(c, views.NewNode(node)) + return utils.Render(c, views.NewPost(node)) +} +func (r *Router) NewTopic(c echo.Context) error { + req := new(nodeRequest) + if err := c.Bind(req); err != nil { + return err + } + user := contextlib.GetUser(c.Request().Context()) + if user == nil { + return echo.ErrForbidden + } + sParentID := c.Param("id") + parentID, err := strconv.Atoi(sParentID) + if err != nil { + return err + } + + if c.Request().Method == http.MethodPost { + postID, err := r.nodeRepo.Create(c.Request().Context(), req.Type, req.Text, user.ID, parentID) + if err != nil { + return err + } + + return c.Redirect(302, fmt.Sprintf("/t/%d", postID)) + } + + node, err := r.nodeRepo.Get(c.Request().Context(), parentID) + if err != nil { + return err + } + + return utils.Render(c, views.NewTopic(node)) } diff --git a/routes/user.go b/routes/user.go index 95d5717..dd5bd61 100644 --- a/routes/user.go +++ b/routes/user.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" + "gitrepo.ru/neonxp/gorum/models" "gitrepo.ru/neonxp/gorum/utils" "gitrepo.ru/neonxp/gorum/views" ) @@ -72,7 +73,7 @@ func (r *Router) Register(c echo.Context) error { return err } if c.Request().Method == http.MethodPost { - uid, err := r.userRepo.Create(c.Request().Context(), req.Email, req.Password, req.Username) + uid, err := r.userRepo.Create(c.Request().Context(), req.Email, req.Password, req.Username, models.RoleUser) if err != nil { return err } diff --git a/views/csrf.templ b/views/csrf.templ index 2bb3545..c019d4c 100644 --- a/views/csrf.templ +++ b/views/csrf.templ @@ -1,7 +1,6 @@ package views -import "github.com/labstack/echo/v4" templ CSRF() { - + // } diff --git a/views/csrf_templ.go b/views/csrf_templ.go new file mode 100644 index 0000000..53a7118 --- /dev/null +++ b/views/csrf_templ.go @@ -0,0 +1,31 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.747 +package views + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func CSRF() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + return templ_7745c5c3_Err + }) +} diff --git a/views/error.templ b/views/error.templ index d3a4a32..3e857ef 100644 --- a/views/error.templ +++ b/views/error.templ @@ -1,8 +1,8 @@ package views templ ErrorPage(err error) { - @Layout() { -

Ошибка

- {err.Error()} - } + @Layout(nil) { +

Ошибка

+ { err.Error() } + } } diff --git a/views/error_templ.go b/views/error_templ.go index 66d40fd..8513d1d 100644 --- a/views/error_templ.go +++ b/views/error_templ.go @@ -38,14 +38,14 @@ func ErrorPage(err error) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Ошибка

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(err.Error()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/error.templ`, Line: 6, Col: 20} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/error.templ`, Line: 6, Col: 15} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -53,7 +53,7 @@ func ErrorPage(err error) templ.Component { } return templ_7745c5c3_Err }) - templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout(nil).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/views/error_templ.txt b/views/error_templ.txt deleted file mode 100644 index d3a792d..0000000 --- a/views/error_templ.txt +++ /dev/null @@ -1 +0,0 @@ -

Ошибка

diff --git a/views/layouts.templ b/views/layouts.templ index 3db6f1b..f1c2deb 100644 --- a/views/layouts.templ +++ b/views/layouts.templ @@ -2,11 +2,12 @@ package views import ( "context" + "fmt" "gitrepo.ru/neonxp/gorum/contextlib" "gitrepo.ru/neonxp/gorum/models" ) -templ Layout() { +templ Layout(parent *models.Node) { @@ -14,7 +15,7 @@ templ Layout() { - + Gorum @@ -23,9 +24,23 @@ templ Layout() {
  • Gorum BBS
  • -
  • - Список тем -
  • + if parent != nil { +
  • + Список тем +
  • +
  • + switch parent.Type { + case models.PostType: + На уровень выше + case models.TopicType: + К предыдущей теме + } +
  • + } else { +
  • + Список тем +
  • + }