diff options
author | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-10-12 02:52:22 +0300 |
---|---|---|
committer | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-10-12 02:53:52 +0300 |
commit | d05ea66f4bbcf0cc5c8908f3435c68de1b070fa1 (patch) | |
tree | 7c7a769206646f2b81a0eda0680f0be5033a4197 /pkg |
Начальная версияv0.0.1
Diffstat (limited to 'pkg')
36 files changed, 1574 insertions, 0 deletions
diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..6a9d30c --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,27 @@ +package config + +import ( + "os" + + "go.neonxp.ru/framework/pkg/db" + "gopkg.in/yaml.v3" +) + +type Config struct { + Debug bool `yaml:"debug"` + DB *db.Config `yaml:"db"` + Listen string `yaml:"listen"` + Host string `yaml:"host"` + Admins map[string]string `yaml:"admins"` + Keypairs []string `yaml:"keys"` +} + +func New(file string) (*Config, error) { + cfg := new(Config) + fp, err := os.Open(file) + if err != nil { + return nil, err + } + + return cfg, yaml.NewDecoder(fp).Decode(cfg) +} diff --git a/pkg/db/config.go b/pkg/db/config.go new file mode 100644 index 0000000..4df38f0 --- /dev/null +++ b/pkg/db/config.go @@ -0,0 +1,5 @@ +package db + +type Config struct { + DSN string `yaml:"dsn"` +} diff --git a/pkg/db/db.go b/pkg/db/db.go new file mode 100644 index 0000000..8b7c47c --- /dev/null +++ b/pkg/db/db.go @@ -0,0 +1,18 @@ +package db + +import ( + "database/sql" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/pgdialect" + "github.com/uptrace/bun/driver/pgdriver" +) + +// dsn := "postgres://postgres:@localhost:5432/test?sslmode=disable" +// dsn := "unix://user:pass@dbname/var/run/postgresql/.s.PGSQL.5432" + +func New(config *Config) *bun.DB { + sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(config.DSN))) + + return bun.NewDB(sqldb, pgdialect.New()) +} diff --git a/pkg/handler/error.go b/pkg/handler/error.go new file mode 100644 index 0000000..eac791d --- /dev/null +++ b/pkg/handler/error.go @@ -0,0 +1,47 @@ +package handler + +import ( + "log" + "net/http" + + "github.com/labstack/echo/v4" + "go.neonxp.ru/framework/pkg/tpl" +) + +func ErrorHandler(err error, c echo.Context) { + if c.Response().Committed { + return + } + + he, ok := err.(*echo.HTTPError) + if ok { + if he.Internal != nil { + if herr, ok := he.Internal.(*echo.HTTPError); ok { + he = herr + } + } + } else { + he = &echo.HTTPError{ + Code: http.StatusInternalServerError, + Message: http.StatusText(http.StatusInternalServerError), + } + } + code := he.Code + message, ok := he.Message.(string) + if !ok { + message = "Неизвестная ошибка" + } + + if c.Request().Method == http.MethodHead { + err = c.NoContent(he.Code) + } else { + c.Response().WriteHeader(code) + if err := tpl.ErrorPage(code, message). + Render(c.Request().Context(), c.Response()); err != nil { + log.Println(err) + } + } + if err != nil { + log.Println(err) + } +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go new file mode 100644 index 0000000..15eb42d --- /dev/null +++ b/pkg/handler/handler.go @@ -0,0 +1,15 @@ +package handler + +import "github.com/labstack/echo/v4" + +type Handler interface { + Register(g *echo.Group) +} + +type Router map[string]Handler + +func (r Router) Register(e *echo.Echo) { + for groupName, handlers := range r { + handlers.Register(e.Group(groupName)) + } +} diff --git a/pkg/handler/user/errors.go b/pkg/handler/user/errors.go new file mode 100644 index 0000000..ab8fc4a --- /dev/null +++ b/pkg/handler/user/errors.go @@ -0,0 +1,30 @@ +package user + +import ( + "errors" + + "go.neonxp.ru/framework/pkg/service/user" +) + +var UserErrors = Errors{ + user.ErrInvalidUserOrPassword: "Неверный email или пароль", + user.ErrPasswordTooShort: "Пароль слишком короткий", + user.ErrPasswordTooWeak: "Пароль слишком простой", + user.ErrUserAlreadyExist: "Пользователь уже существует", + user.ErrUsernameToShort: "Имя пользователя слишком короткое", + // user.ErrEmailEmpty: "Электропочта не указана", + // user.ErrPasswordEmpty: "Пароль не указан", + // user.ErrNameEmpty: "Имя пользователя не указано", +} + +type Errors map[error]string + +func (e Errors) Get(err error) string { + for target, msg := range e { + if errors.Is(err, target) { + return msg + } + } + + return "" +} diff --git a/pkg/handler/user/handler.go b/pkg/handler/user/handler.go new file mode 100644 index 0000000..6bc8bff --- /dev/null +++ b/pkg/handler/user/handler.go @@ -0,0 +1,24 @@ +package user + +import ( + "github.com/labstack/echo/v4" + "go.neonxp.ru/framework/pkg/service/user" +) + +type Handler struct { + user *user.Service +} + +// NewHandler returns new Handler. +func NewHandler(u *user.Service) *Handler { + return &Handler{user: u} +} + +func (h *Handler) Register(g *echo.Group) { + g.GET("/login", h.LoginForm) + g.POST("/login", h.LoginForm) + g.GET("/register", h.RegisterForm) + g.POST("/register", h.RegisterForm) + g.GET("/profile", h.Profile) + g.POST("/logout", h.Logout) +} diff --git a/pkg/handler/user/login.go b/pkg/handler/user/login.go new file mode 100644 index 0000000..b3fbdbf --- /dev/null +++ b/pkg/handler/user/login.go @@ -0,0 +1,53 @@ +package user + +import ( + "net/http" + + "github.com/labstack/echo/v4" + + "go.neonxp.ru/framework/pkg/tpl" + "go.neonxp.ru/framework/pkg/utils" +) + +const oneyear = 86400 * 365 + +func (h *Handler) LoginForm(c echo.Context) error { + form := &tpl.LoginForm{} + if err := c.Bind(form); err != nil { + return err + } + + if c.Request().Method == http.MethodPost { + err := h.doLogin(c, form) + if err == nil { + if utils.IsHTMX(c) { + utils.HTMXRedirect(c, "/") + return c.NoContent(http.StatusNoContent) + } + + return c.Redirect(http.StatusFound, "/") + } + + form.Message = UserErrors.Get(err) + if form.Message == "" { + return err + } + } + + return tpl.Login(form). + Render(c.Request().Context(), c.Response()) +} + +func (h *Handler) doLogin(c echo.Context, form *tpl.LoginForm) error { + u, err := h.user.Login(c.Request().Context(), form) + if err != nil { + return err + } + + maxage := 0 + if form.Remember == "on" { + maxage = oneyear + } + + return utils.SetUser(c, u, maxage) +} diff --git a/pkg/handler/user/logout.go b/pkg/handler/user/logout.go new file mode 100644 index 0000000..9b7ea5f --- /dev/null +++ b/pkg/handler/user/logout.go @@ -0,0 +1,18 @@ +package user + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "go.neonxp.ru/framework/pkg/utils" +) + +func (*Handler) Logout(c echo.Context) error { + if err := utils.SetUser(c, nil, -1); err != nil { + return err + } + + utils.HTMXRedirect(c, "/") + + return c.NoContent(http.StatusNoContent) +} diff --git a/pkg/handler/user/profile.go b/pkg/handler/user/profile.go new file mode 100644 index 0000000..7181ca7 --- /dev/null +++ b/pkg/handler/user/profile.go @@ -0,0 +1,16 @@ +package user + +import ( + "github.com/labstack/echo/v4" + "go.neonxp.ru/framework/pkg/tpl" + "go.neonxp.ru/framework/pkg/utils" +) + +func (*Handler) Profile(c echo.Context) error { + u := utils.GetUserCtx(c.Request().Context()) + if u == nil { + return echo.ErrForbidden + } + + return tpl.Profile(u).Render(c.Request().Context(), c.Response()) +} diff --git a/pkg/handler/user/register.go b/pkg/handler/user/register.go new file mode 100644 index 0000000..2bd810a --- /dev/null +++ b/pkg/handler/user/register.go @@ -0,0 +1,45 @@ +package user + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "go.neonxp.ru/framework/pkg/tpl" + "go.neonxp.ru/framework/pkg/utils" +) + +func (h *Handler) RegisterForm(c echo.Context) error { + form := &tpl.RegisterForm{} + if err := c.Bind(form); err != nil { + return err + } + + if c.Request().Method == http.MethodPost { + err := h.doRegister(c, form) + if err == nil { + if utils.IsHTMX(c) { + utils.HTMXRedirect(c, "/") + return c.NoContent(http.StatusNoContent) + } + + return c.Redirect(http.StatusFound, "/") + } + + form.Message = UserErrors.Get(err) + if form.Message == "" { + return err + } + } + + return tpl.Register(form). + Render(c.Request().Context(), c.Response()) +} + +func (h *Handler) doRegister(c echo.Context, form *tpl.RegisterForm) error { + u, err := h.user.Register(c.Request().Context(), form) + if err != nil { + return err + } + + return utils.SetUser(c, u, oneyear) +} diff --git a/pkg/middleware/context.go b/pkg/middleware/context.go new file mode 100644 index 0000000..f9c4425 --- /dev/null +++ b/pkg/middleware/context.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "context" + + "github.com/labstack/echo/v4" +) + +type ContextKey string + +func Context(key ContextKey, value any) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + ctx := context.WithValue(c.Request().Context(), key, value) + r := c.Request().WithContext(ctx) + c.SetRequest(r) + + return next(c) + } + } +} diff --git a/pkg/middleware/session/store.go b/pkg/middleware/session/store.go new file mode 100644 index 0000000..78172d7 --- /dev/null +++ b/pkg/middleware/session/store.go @@ -0,0 +1,232 @@ +package session + +import ( + "context" + "encoding/base32" + "log/slog" + "net/http" + "strings" + "time" + + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" + "github.com/uptrace/bun" +) + +const ( + sessionIDLen = 32 + defaultTableName = "sessions" + defaultMaxAge = 60 * 60 * 24 * 30 // 30 days + defaultPath = "/" +) + +// Options for bunstore. +type Options struct { + TableName string + SkipCreateTable bool +} + +// Store represent a bunstore. +type Store struct { + db *bun.DB + opts Options + Codecs []securecookie.Codec + SessionOpts *sessions.Options +} + +type Model struct { + bun.BaseModel `bun:"table:sessions,alias:s"` + + ID string `bun:",pk,unique"` + Data string + CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + ExpiresAt time.Time +} + +type KeyPairs []string + +func (k KeyPairs) ToKeys() [][]byte { + b := make([][]byte, 0, len(k)) + for _, kk := range k { + b = append(b, []byte(kk)) + } + + return b +} + +// New creates a new bunstore session. +func New(db *bun.DB, keyPairs KeyPairs) (*Store, error) { + return NewOptions(db, Options{}, keyPairs) +} + +// NewOptions creates a new bunstore session with options. +func NewOptions(db *bun.DB, opts Options, keyPairs KeyPairs) (*Store, error) { + st := &Store{ + db: db, + opts: opts, + Codecs: securecookie.CodecsFromPairs(keyPairs.ToKeys()...), + SessionOpts: &sessions.Options{ + Path: defaultPath, + MaxAge: defaultMaxAge, + }, + } + + return st, nil +} + +// Get returns a session for the given name after adding it to the registry. +func (st *Store) Get(r *http.Request, name string) (*sessions.Session, error) { + return sessions.GetRegistry(r).Get(st, name) +} + +// New creates a session with name without adding it to the registry. +func (st *Store) New(r *http.Request, name string) (*sessions.Session, error) { + session := sessions.NewSession(st, name) + opts := *st.SessionOpts + session.Options = &opts + session.IsNew = true + + st.MaxAge(st.SessionOpts.MaxAge) + + // try fetch from db if there is a cookie + s := st.getSessionFromCookie(r, session.Name()) + if s != nil { + if err := securecookie.DecodeMulti(session.Name(), s.Data, &session.Values, st.Codecs...); err != nil { + //nolint:nilerr + return session, nil + } + + session.ID = s.ID + session.IsNew = false + } + + return session, nil +} + +// Save session and set cookie header. +func (st *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { + s := st.getSessionFromCookie(r, session.Name()) + + // delete if max age is < 0 + if session.Options.MaxAge < 0 { + if s != nil { + if _, err := st.db.NewDelete().Model(&Model{ID: session.ID}).WherePK("id").Exec(r.Context()); err != nil { + return err + } + } + + http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options)) + + return nil + } + + data, err := securecookie.EncodeMulti(session.Name(), session.Values, st.Codecs...) + if err != nil { + return err + } + + now := time.Now() + expire := now.Add(time.Second * time.Duration(session.Options.MaxAge)) + + if s == nil { + // generate random session ID key suitable for storage in the db + session.ID = strings.TrimRight( + base32.StdEncoding.EncodeToString( + securecookie.GenerateRandomKey(sessionIDLen)), "=") + s = &Model{ + ID: session.ID, + Data: data, + ExpiresAt: expire, + } + + if _, err := st.db.NewInsert().Model(s).Exec(r.Context()); err != nil { + return err + } + } else { + s.Data = data + s.ExpiresAt = expire + + if _, err := st.db.NewUpdate().Model(s).WherePK("id").Column("data", "expires_at").Exec(r.Context()); err != nil { + return err + } + } + + // set session id cookie + id, err := securecookie.EncodeMulti(session.Name(), s.ID, st.Codecs...) + if err != nil { + return err + } + + http.SetCookie(w, sessions.NewCookie(session.Name(), id, session.Options)) + + return nil +} + +// getSessionFromCookie looks for an existing bunSession from a session ID stored inside a cookie. +func (st *Store) getSessionFromCookie(r *http.Request, name string) *Model { + if cookie, err := r.Cookie(name); err == nil { + sessionID := "" + if err := securecookie.DecodeMulti(name, cookie.Value, &sessionID, st.Codecs...); err != nil { + return nil + } + + s := &Model{} + if err := st.db.NewSelect(). + Model(s). + Where("id = ? AND expires_at > ?", sessionID, time.Now()). + Scan(r.Context()); err != nil { + return nil + } + + return s + } + + return nil +} + +// MaxAge sets the maximum age for the store and the underlying cookie +// implementation. Individual sessions can be deleted by setting +// Options.MaxAge = -1 for that session. +func (st *Store) MaxAge(age int) { + st.SessionOpts.MaxAge = age + for _, codec := range st.Codecs { + if sc, ok := codec.(*securecookie.SecureCookie); ok { + sc.MaxAge(age) + } + } +} + +// MaxLength restricts the maximum length of new sessions to l. +// If l is 0 there is no limit to the size of a session, use with caution. +// The default is 4096 (default for securecookie). +func (st *Store) MaxLength(l int) { + for _, c := range st.Codecs { + if codec, ok := c.(*securecookie.SecureCookie); ok { + codec.MaxLength(l) + } + } +} + +// Cleanup deletes expired sessions. +func (st *Store) Cleanup() { + _, err := st.db.NewDelete().Model(&Model{}).Where("expires_at <= ?", time.Now()).Exec(context.Background()) + if err != nil { + slog.Default().With("error", err).Error("cleanup") + } +} + +// PeriodicCleanup runs Cleanup every interval. Close quit channel to stop. +func (st *Store) PeriodicCleanup(interval time.Duration, quit <-chan struct{}) { + t := time.NewTicker(interval) + defer t.Stop() + + for { + select { + case <-t.C: + st.Cleanup() + case <-quit: + return + } + } +} diff --git a/pkg/middleware/user.go b/pkg/middleware/user.go new file mode 100644 index 0000000..6271e43 --- /dev/null +++ b/pkg/middleware/user.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "context" + + "github.com/labstack/echo-contrib/session" + "github.com/labstack/echo/v4" +) + +func User() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + sess, err := session.Get("user", c) + if err != nil { + return err + } + + u := sess.Values["user"] + c.Set("user", u) + + ctx := context.WithValue(c.Request().Context(), ContextKey("user"), u) + r := c.Request().WithContext(ctx) + c.SetRequest(r) + + return next(c) + } + } +} diff --git a/pkg/model/user.go b/pkg/model/user.go new file mode 100644 index 0000000..ec524a8 --- /dev/null +++ b/pkg/model/user.go @@ -0,0 +1,26 @@ +package model + +import ( + "encoding/gob" + "encoding/json" + "time" + + "github.com/uptrace/bun" +) + +//nolint:gochecknoinits +func init() { + gob.Register(User{}) +} + +type User struct { + bun.BaseModel `bun:"table:users,alias:u"` + + ID int64 `bun:",pk,autoincrement"` + Email string `bun:",notnull,unique"` + Username string `bun:",notnull,unique"` + Password string `bun:",notnull"` + Meta json.RawMessage `bun:",type:jsonb"` + CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + DeletedAt time.Time `bun:",soft_delete,nullzero"` +} diff --git a/pkg/service/crud/crud.go b/pkg/service/crud/crud.go new file mode 100644 index 0000000..799eacb --- /dev/null +++ b/pkg/service/crud/crud.go @@ -0,0 +1,87 @@ +package crud + +import ( + "context" + "errors" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/driver/pgdriver" +) + +var ErrRecordAlreadyExists = errors.New("record already exists") + +type Service[T any] struct { + db *bun.DB +} + +func NewService[T any](db *bun.DB) *Service[T] { + return &Service[T]{ + db: db, + } +} + +func (s *Service[T]) Insert(ctx context.Context, model *T) error { + if _, err := s.db.NewInsert().Model(model).Returning("*").Exec(ctx); err != nil { + pqErr := pgdriver.Error{} + if errors.As(err, &pqErr) { + if pqErr.Field('C') == "23505" { + return ErrRecordAlreadyExists + } + } + + return err + } + + return nil +} + +func (s *Service[T]) UpdatePk(ctx context.Context, model *T, columns []string) error { + if _, err := s.db.NewUpdate().Model(model).WherePK(columns...).Column(columns...).Exec(ctx); err != nil { + pqErr := pgdriver.Error{} + if errors.As(err, &pqErr) { + if pqErr.Field('C') == "23505" { + return ErrRecordAlreadyExists + } + } + + return err + } + + return nil +} + +//nolint:revive +func (s *Service[T]) Update(ctx context.Context, model *T, columns []string, query string, args ...any) error { + if _, err := s.db.NewUpdate().Model(model).Where(query, args...).Column(columns...).Exec(ctx); err != nil { + pqErr := pgdriver.Error{} + if errors.As(err, &pqErr) { + if pqErr.Field('C') == "23505" { + return ErrRecordAlreadyExists + } + } + + return err + } + + return nil +} + +func (s *Service[T]) FindOne(ctx context.Context, query string, args ...any) (*T, error) { + m := new(T) + + return m, s.db.NewSelect().Model(m).Where(query, args...).Scan(ctx, m) +} + +//nolint:revive +func (s *Service[T]) Find(ctx context.Context, query string, args ...any) ([]T, int, error) { + m := make([]T, 0) + c, err := s.db.NewSelect().Model(m).Where(query, args...).ScanAndCount(ctx, &m) + + return m, c, err +} + +func (s *Service[T]) Delete(ctx context.Context, query string, args ...any) error { + _, err := s.db.NewDelete().Model((*T)(nil)).Where(query, args...).Exec(ctx) + + return err +} diff --git a/pkg/service/user/login.go b/pkg/service/user/login.go new file mode 100644 index 0000000..d1f5c65 --- /dev/null +++ b/pkg/service/user/login.go @@ -0,0 +1,31 @@ +package user + +import ( + "context" + "errors" + + normalizer "github.com/dimuska139/go-email-normalizer/v3" + "golang.org/x/crypto/bcrypt" + + "go.neonxp.ru/framework/pkg/model" + "go.neonxp.ru/framework/pkg/tpl" +) + +var ErrInvalidUserOrPassword = errors.New("invalid_user_or_password") + +func (s *Service) Login(ctx context.Context, form *tpl.LoginForm) (*model.User, error) { + n := normalizer.NewNormalizer() + u := &model.User{ + Email: n.Normalize(form.Email), + } + + if err := s.db.NewSelect().Model(u).Where("email = ?", u.Email).Scan(ctx, u); err != nil { + return nil, errors.Join(err, ErrInvalidUserOrPassword) + } + + if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(form.Password)); err != nil { + return nil, errors.Join(err, ErrInvalidUserOrPassword) + } + + return u, nil +} diff --git a/pkg/service/user/register.go b/pkg/service/user/register.go new file mode 100644 index 0000000..69f19a2 --- /dev/null +++ b/pkg/service/user/register.go @@ -0,0 +1,85 @@ +package user + +import ( + "context" + "errors" + "regexp" + + normalizer "github.com/dimuska139/go-email-normalizer/v3" + "github.com/uptrace/bun/driver/pgdriver" + "golang.org/x/crypto/bcrypt" + + "go.neonxp.ru/framework/pkg/model" + "go.neonxp.ru/framework/pkg/tpl" +) + +var ( + ErrUsernameToShort = errors.New("username_to_short") + ErrUserAlreadyExist = errors.New("user_already_exists") + ErrPasswordTooWeak = errors.New("password_too_weak") + ErrPasswordTooShort = errors.New("password_too_short") +) + +const ( + minUsernameLen = 3 + minPasswordLen = 8 +) + +func (s *Service) Register(ctx context.Context, form *tpl.RegisterForm) (*model.User, error) { + if len(form.Username) < minUsernameLen { + return nil, ErrUsernameToShort + } + + if err := checkPasswordLever(form.Password); err != nil { + return nil, err + } + + password, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + n := normalizer.NewNormalizer() + u := &model.User{ + Username: form.Username, + Email: n.Normalize(form.Email), + Password: string(password), + } + + if _, err := s.db.NewInsert().Model(u).Returning("*").Exec(ctx); err != nil { + pqErr := pgdriver.Error{} + if errors.As(err, &pqErr) { + if pqErr.Field('C') == "23505" { + return nil, ErrUserAlreadyExist + } + } + + return nil, err + } + + return u, nil +} + +func checkPasswordLever(ps string) error { + if len(ps) < minPasswordLen { + return ErrPasswordTooShort + } + + lowerCase := `[a-z]{1}` + upperCase := `[A-Z]{1}` + symbol := `[0-9!@#~$%^&*()+|_]{1}` + + if b, err := regexp.MatchString(lowerCase, ps); !b || err != nil { + return ErrPasswordTooWeak + } + + if b, err := regexp.MatchString(upperCase, ps); !b || err != nil { + return ErrPasswordTooWeak + } + + if b, err := regexp.MatchString(symbol, ps); !b || err != nil { + return ErrPasswordTooWeak + } + + return nil +} diff --git a/pkg/service/user/user.go b/pkg/service/user/user.go new file mode 100644 index 0000000..aa27eee --- /dev/null +++ b/pkg/service/user/user.go @@ -0,0 +1,12 @@ +package user + +import "github.com/uptrace/bun" + +type Service struct { + db *bun.DB +} + +// NewService returns new Service. +func NewService(db *bun.DB) *Service { + return &Service{db: db} +} diff --git a/pkg/tpl/error.templ b/pkg/tpl/error.templ new file mode 100644 index 0000000..0e0dcfa --- /dev/null +++ b/pkg/tpl/error.templ @@ -0,0 +1,10 @@ +package tpl + +import "strconv" + +templ ErrorPage(code int, message string) { + @Layout() { + <h1>Ошибка { strconv.Itoa(code) }!</h1> + <p>{ message }</p> + } +} diff --git a/pkg/tpl/error_templ.go b/pkg/tpl/error_templ.go new file mode 100644 index 0000000..04b4921 --- /dev/null +++ b/pkg/tpl/error_templ.go @@ -0,0 +1,86 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.778 +package tpl + +//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" + +import "strconv" + +func ErrorPage(code int, message string) 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 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + 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) + templ_7745c5c3_Var2 := 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_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(code)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/error.templ`, Line: 7, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/error.templ`, Line: 8, Col: 14} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) + templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/tpl/error_templ.txt b/pkg/tpl/error_templ.txt new file mode 100644 index 0000000..5d1d217 --- /dev/null +++ b/pkg/tpl/error_templ.txt @@ -0,0 +1,3 @@ +<h1>Ошибка +!</h1><p> +</p> diff --git a/pkg/tpl/layout.templ b/pkg/tpl/layout.templ new file mode 100644 index 0000000..4993b97 --- /dev/null +++ b/pkg/tpl/layout.templ @@ -0,0 +1,49 @@ +package tpl + +import "go.neonxp.ru/framework/pkg/utils" + +templ Layout() { + {{ user := utils.GetUserCtx(ctx) }} + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <meta name="color-scheme" content="light dark"/> + <link rel="stylesheet" href="/css/pico/pico.blue.min.css"/> + <link rel="stylesheet" href="/css/style.css"/> + <link rel="stylesheet" href="/css/fork-awesome.min.css"/> + <title>App</title> + </head> + <body> + <main class="container"> + <nav> + <ul> + <li><a href="/"><strong>App</strong></a></li> + <li><span aria-busy="true" id="loader" class="htmx-indicator">Загрузка...</span></li> + </ul> + <ul hx-boost="true" hx-indicator="#loader"> + if user == nil { + <li><a href="/user/login">Вход</a></li> + <li><a href="/user/register">Регистрация</a></li> + } else { + <li><a href="/user/profile">{ user.Username }</a></li> + <li><a href="#" hx-post="/user/logout">Выход</a></li> + } + </ul> + </nav> + { children... } + </main> + </body> + <footer> + <main class="container"> + <nav> + <ul> + <li>Сделал <a href="https://neonxp.ru/">NeonXP</a> в 2024 году.</li> + </ul> + </nav> + </main> + </footer> + <script src="/js/htmx.min.js"></script> + </html> +} diff --git a/pkg/tpl/layout_templ.go b/pkg/tpl/layout_templ.go new file mode 100644 index 0000000..1edca5a --- /dev/null +++ b/pkg/tpl/layout_templ.go @@ -0,0 +1,79 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.778 +package tpl + +//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" + +import "go.neonxp.ru/framework/pkg/utils" + +func Layout() 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 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + 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) + user := utils.GetUserCtx(ctx) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if user == nil { + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/layout.templ`, Line: 30, Col: 50} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 6) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/tpl/layout_templ.txt b/pkg/tpl/layout_templ.txt new file mode 100644 index 0000000..c6fb6f2 --- /dev/null +++ b/pkg/tpl/layout_templ.txt @@ -0,0 +1,6 @@ +<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"light dark\"><link rel=\"stylesheet\" href=\"/css/pico/pico.blue.min.css\"><link rel=\"stylesheet\" href=\"/css/style.css\"><link rel=\"stylesheet\" href=\"/css/fork-awesome.min.css\"><title>App</title></head><body><main class=\"container\"><nav><ul><li><a href=\"/\"><strong>App</strong></a></li><li><span aria-busy=\"true\" id=\"loader\" class=\"htmx-indicator\">Загрузка...</span></li></ul><ul hx-boost=\"true\" hx-indicator=\"#loader\"> +<li><a href=\"/user/login\">Вход</a></li><li><a href=\"/user/register\">Регистрация</a></li> +<li><a href=\"/user/profile\"> +</a></li><li><a href=\"#\" hx-post=\"/user/logout\">Выход</a></li> +</ul></nav> +</main></body><footer><main class=\"container\"><nav><ul><li>Сделал <a href=\"https://neonxp.ru/\">NeonXP</a> в 2024 году.</li></ul></nav></main></footer><script src=\"/js/htmx.min.js\"></script></html> diff --git a/pkg/tpl/login.templ b/pkg/tpl/login.templ new file mode 100644 index 0000000..1f28e4d --- /dev/null +++ b/pkg/tpl/login.templ @@ -0,0 +1,60 @@ +package tpl + +templ Login(form *LoginForm) { + @Layout() { + <article class="grid"> + <div> + <hgroup> + <h1>Вход</h1> + <p>Вход в систему</p> + </hgroup> + <form + method="post" + hx-post="/user/login" + hx-target="form" + hx-select="form" + hx-indicator="#loader" + > + if form.Message != "" { + <article> + <header>Ошибка</header> + { form.Message } + </article> + } + <input + type="email" + name="email" + placeholder="Электропочта" + aria-label="Электропочта" + autocomplete="email" + value={ form.Email } + required + /> + <input + type="password" + name="password" + placeholder="Пароль" + aria-label="Пароль" + autocomplete="current-password" + required + /> + <fieldset> + <label for="remember"> + <input type="checkbox" role="switch" id="remember" name="remember" checked={ form.Remember }/> + Запомнить + </label> + </fieldset> + <button type="submit" class="contrast">Вход</button> + </form> + </div> + <div></div> + </article> + } +} + +type LoginForm struct { + Message string + Email string `form:"email"` + Password string `form:"password"` + Remember string `form:"remember"` +} diff --git a/pkg/tpl/login_templ.go b/pkg/tpl/login_templ.go new file mode 100644 index 0000000..8c02424 --- /dev/null +++ b/pkg/tpl/login_templ.go @@ -0,0 +1,114 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.778 +package tpl + +//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 Login(form *LoginForm) 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 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + 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) + templ_7745c5c3_Var2 := 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_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if form.Message != "" { + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(form.Message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/login.templ`, Line: 21, Col: 21} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/login.templ`, Line: 30, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(form.Remember) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/login.templ`, Line: 43, Col: 97} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 6) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) + templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +type LoginForm struct { + Message string + Email string `form:"email"` + Password string `form:"password"` + Remember string `form:"remember"` +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/tpl/login_templ.txt b/pkg/tpl/login_templ.txt new file mode 100644 index 0000000..af8cbb3 --- /dev/null +++ b/pkg/tpl/login_templ.txt @@ -0,0 +1,6 @@ +<article class=\"grid\"><div><hgroup><h1>Вход</h1><p>Вход в систему</p></hgroup><form method=\"post\" hx-post=\"/user/login\" hx-target=\"form\" hx-select=\"form\" hx-indicator=\"#loader\"> +<article><header>Ошибка</header> +</article> +<input type=\"email\" name=\"email\" placeholder=\"Электропочта\" aria-label=\"Электропочта\" autocomplete=\"email\" value=\" +\" required> <input type=\"password\" name=\"password\" placeholder=\"Пароль\" aria-label=\"Пароль\" autocomplete=\"current-password\" required><fieldset><label for=\"remember\"><input type=\"checkbox\" role=\"switch\" id=\"remember\" name=\"remember\" checked=\" +\"> Запомнить</label></fieldset><button type=\"submit\" class=\"contrast\">Вход</button></form></div><div></div></article> diff --git a/pkg/tpl/profile.templ b/pkg/tpl/profile.templ new file mode 100644 index 0000000..61a9221 --- /dev/null +++ b/pkg/tpl/profile.templ @@ -0,0 +1,17 @@ +package tpl + +import "go.neonxp.ru/framework/pkg/model" + +templ Profile(user *model.User) { + @Layout() { + <article class="grid"> + <div> + <hgroup> + <h1>{ user.Username }</h1> + <p>{ user.Email }</p> + </hgroup> + </div> + <div></div> + </article> + } +} diff --git a/pkg/tpl/profile_templ.go b/pkg/tpl/profile_templ.go new file mode 100644 index 0000000..e51ab9a --- /dev/null +++ b/pkg/tpl/profile_templ.go @@ -0,0 +1,86 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.778 +package tpl + +//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" + +import "go.neonxp.ru/framework/pkg/model" + +func Profile(user *model.User) 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 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + 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) + templ_7745c5c3_Var2 := 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_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/profile.templ`, Line: 10, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/profile.templ`, Line: 11, Col: 20} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) + templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/tpl/profile_templ.txt b/pkg/tpl/profile_templ.txt new file mode 100644 index 0000000..5fa238c --- /dev/null +++ b/pkg/tpl/profile_templ.txt @@ -0,0 +1,3 @@ +<article class=\"grid\"><div><hgroup><h1> +</h1><p> +</p></hgroup></div><div></div></article> diff --git a/pkg/tpl/register.templ b/pkg/tpl/register.templ new file mode 100644 index 0000000..c7c7210 --- /dev/null +++ b/pkg/tpl/register.templ @@ -0,0 +1,63 @@ +package tpl + +templ Register(form *RegisterForm) { + @Layout() { + <article class="grid"> + <div> + <hgroup> + <h1>Регистрация</h1> + <p>Регистрация в системе</p> + </hgroup> + <form + method="post" + hx-post="/user/register" + hx-target="form" + hx-select="form" + hx-indicator="#loader" + > + if form.Message != "" { + <article> + <header>Ошибка</header> + { form.Message } + </article> + } + <input + type="text" + name="username" + placeholder="Отображаемое имя" + aria-label="Отображаемое имя" + autocomplete="username" + value={ form.Username } + required + /> + <input + type="email" + name="email" + placeholder="Электропочта" + aria-label="Электропочта" + autocomplete="email" + value={ form.Email } + required + /> + <input + type="password" + name="password" + placeholder="Пароль" + aria-label="Пароль" + autocomplete="current-password" + required + /> + <button type="submit" class="contrast">Регистрация</button> + </form> + </div> + <div></div> + </article> + } +} + +type RegisterForm struct { + Message string + Username string `form:"username"` + Email string `form:"email"` + Password string `form:"password"` +} diff --git a/pkg/tpl/register_templ.go b/pkg/tpl/register_templ.go new file mode 100644 index 0000000..ffab2de --- /dev/null +++ b/pkg/tpl/register_templ.go @@ -0,0 +1,114 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.778 +package tpl + +//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 Register(form *RegisterForm) 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 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + 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) + templ_7745c5c3_Var2 := 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_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if form.Message != "" { + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(form.Message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/register.templ`, Line: 21, Col: 21} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(form.Username) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/register.templ`, Line: 30, Col: 27} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/register.templ`, Line: 39, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 6) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) + templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +type RegisterForm struct { + Message string + Username string `form:"username"` + Email string `form:"email"` + Password string `form:"password"` +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/tpl/register_templ.txt b/pkg/tpl/register_templ.txt new file mode 100644 index 0000000..c404586 --- /dev/null +++ b/pkg/tpl/register_templ.txt @@ -0,0 +1,6 @@ +<article class=\"grid\"><div><hgroup><h1>Регистрация</h1><p>Регистрация в системе</p></hgroup><form method=\"post\" hx-post=\"/user/register\" hx-target=\"form\" hx-select=\"form\" hx-indicator=\"#loader\"> +<article><header>Ошибка</header> +</article> +<input type=\"text\" name=\"username\" placeholder=\"Отображаемое имя\" aria-label=\"Отображаемое имя\" autocomplete=\"username\" value=\" +\" required> <input type=\"email\" name=\"email\" placeholder=\"Электропочта\" aria-label=\"Электропочта\" autocomplete=\"email\" value=\" +\" required> <input type=\"password\" name=\"password\" placeholder=\"Пароль\" aria-label=\"Пароль\" autocomplete=\"current-password\" required> <button type=\"submit\" class=\"contrast\">Регистрация</button></form></div><div></div></article> diff --git a/pkg/utils/htmx.go b/pkg/utils/htmx.go new file mode 100644 index 0000000..c4120f3 --- /dev/null +++ b/pkg/utils/htmx.go @@ -0,0 +1,11 @@ +package utils + +import "github.com/labstack/echo/v4" + +func IsHTMX(c echo.Context) bool { + return c.Request().Header.Get("HX-Request") == "true" +} + +func HTMXRedirect(c echo.Context, location string) { + c.Response().Header().Set("HX-Redirect", location) +} diff --git a/pkg/utils/user.go b/pkg/utils/user.go new file mode 100644 index 0000000..4006b3c --- /dev/null +++ b/pkg/utils/user.go @@ -0,0 +1,41 @@ +package utils + +import ( + "context" + + "github.com/gorilla/sessions" + "github.com/labstack/echo-contrib/session" + "github.com/labstack/echo/v4" + "go.neonxp.ru/framework/pkg/middleware" + "go.neonxp.ru/framework/pkg/model" +) + +func GetUserCtx(ctx context.Context) *model.User { + u := ctx.Value(middleware.ContextKey("user")) + if u == nil { + return nil + } + + if u, ok := u.(model.User); ok { + return &u + } + + return nil +} + +func SetUser(c echo.Context, u *model.User, maxage int) error { + sess, err := session.Get("user", c) + if err != nil { + return err + } + + sess.Values["user"] = u + sess.Options = &sessions.Options{ + Path: "/", + MaxAge: maxage, + HttpOnly: true, + Secure: true, + } + + return sess.Save(c.Request(), c.Response()) +} |