diff options
author | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-08-18 13:29:54 +0300 |
---|---|---|
committer | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-08-18 13:29:54 +0300 |
commit | fd70f95224374d23157ee7c0357733102cd0df53 (patch) | |
tree | e490c12e021cedaf211b292d5d623baa32a673fc /pkg |
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/app/app.go | 90 | ||||
-rw-r--r-- | pkg/config/config.go | 16 | ||||
-rw-r--r-- | pkg/handler/location.go | 141 | ||||
-rw-r--r-- | pkg/handler/user.go | 155 | ||||
-rw-r--r-- | pkg/models/point.go | 18 | ||||
-rw-r--r-- | pkg/models/user.go | 17 |
6 files changed, 437 insertions, 0 deletions
diff --git a/pkg/app/app.go b/pkg/app/app.go new file mode 100644 index 0000000..9a2df50 --- /dev/null +++ b/pkg/app/app.go @@ -0,0 +1,90 @@ +package app + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/alexedwards/scs/boltstore" + "github.com/alexedwards/scs/v2" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + session "github.com/spazzymoto/echo-scs-session" + "gitrepo.ru/neonxp/track/pkg/config" + "gitrepo.ru/neonxp/track/pkg/handler" + "gitrepo.ru/neonxp/track/pkg/models" + "go.etcd.io/bbolt" + bolt "go.etcd.io/bbolt" + "golang.org/x/crypto/bcrypt" +) + +func App(ctx context.Context) error { + cfg := config.New() + db, err := bolt.Open(cfg.DBPath, 0600, nil) + if err != nil { + return fmt.Errorf("failed open db: %w", err) + } + defer db.Close() + + e := echo.New() + + sessionManager := scs.New() + sessionManager.Store = boltstore.NewWithCleanupInterval(db, 5*time.Minute) + sessionManager.Lifetime = 30 * 24 * time.Hour + userHandler := handler.NewUser(db, sessionManager) + locationHandler := handler.NewLocation(db, sessionManager) + + authFunc := func(s1, s2 string, c echo.Context) (bool, error) { + // TODO remove this shit + user := new(models.User) + s1 = strings.ReplaceAll(s1, "%40", "@") + + err := db.View(func(tx *bbolt.Tx) error { + users := tx.Bucket([]byte("users")) + jb := users.Get([]byte(strings.ToLower(s1))) + if jb == nil { + return errors.New("invalid user or password") + } + if err := json.Unmarshal(jb, user); err != nil { + return err + } + if err := bcrypt.CompareHashAndPassword(user.Password, []byte(s2)); err != nil { + return errors.New("invalid user or password") + } + + return nil + }) + if err != nil { + return false, err + } + sessionManager.Put(c.Request().Context(), "user", user) + + return true, nil + } + + func(eg *echo.Group) { + eg.POST("/user/register", userHandler.Register) + eg.POST("/user/login", userHandler.Login) + eg.GET("/user", userHandler.User) + + eg.GET("/point", locationHandler.AddPoint, middleware.BasicAuth(authFunc)) + eg.GET("/points", locationHandler.GetPoints) + eg.GET("/points/last", locationHandler.GetLast) + }(e.Group("/api")) + e.Static("/", "./web") + e.Use( + middleware.Recover(), + middleware.Logger(), + session.LoadAndSave(sessionManager), + ) + + if err := e.Start(cfg.Listen); err != http.ErrServerClosed { + return err + } + + return nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..b745a60 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,16 @@ +package config + +import "os" + +type Config struct { + DBPath string + Listen string +} + +func New() *Config { + cfg := new(Config) + cfg.DBPath = os.Getenv("DB_PATH") + cfg.Listen = os.Getenv("LISTEN") + + return cfg +} diff --git a/pkg/handler/location.go b/pkg/handler/location.go new file mode 100644 index 0000000..9f4a2c1 --- /dev/null +++ b/pkg/handler/location.go @@ -0,0 +1,141 @@ +package handler + +import ( + "encoding/json" + "time" + + "github.com/alexedwards/scs/v2" + "github.com/labstack/echo/v4" + "gitrepo.ru/neonxp/track/pkg/models" + "go.etcd.io/bbolt" + "go.neonxp.ru/objectid" +) + +type Location struct { + db *bbolt.DB + sessions *scs.SessionManager +} + +func NewLocation(db *bbolt.DB, sessions *scs.SessionManager) *Location { + return &Location{ + db: db, + sessions: sessions, + } +} + +func (l *Location) AddPoint(c echo.Context) error { + uu := l.sessions.Get(c.Request().Context(), "user") + if uu == nil { + return echo.ErrForbidden + } + user, ok := uu.(*models.User) + if !ok { + return echo.ErrForbidden + } + + req := new(PointArgs) + if err := c.Bind(req); err != nil { + return err + } + + point := &models.Point{ + ID: objectid.FromTime(req.Time), + UserID: user.ID, + Lat: req.Lat, + Lon: req.Lon, + Time: req.Time, + Speed: req.Speed, + Direction: req.Direction, + Accuracy: req.Accuracy, + } + + err := l.db.Update(func(tx *bbolt.Tx) error { + points, err := tx.CreateBucketIfNotExists([]byte("points")) + if err != nil { + return err + } + jp, err := json.Marshal(point) + if err != nil { + return err + } + + return points.Put(point.ID, jp) + }) + if err != nil { + return err + } + + return c.NoContent(201) +} + +func (l *Location) GetPoints(c echo.Context) error { + // uu := l.sessions.Get(c.Request().Context(), "user") + // if uu == nil { + // return echo.ErrForbidden + // } + // user, ok := uu.(*models.User) + // if !ok { + // return echo.ErrForbidden + // } + + pointsResp := []models.Point{} + err := l.db.View(func(tx *bbolt.Tx) error { + points := tx.Bucket([]byte("points")) + point := new(models.Point) + return points.ForEach(func(k, v []byte) error { + if err := json.Unmarshal(v, point); err != nil { + return err + } + // if slices.Equal(point.UserID, user.ID) { + pointsResp = append(pointsResp, *point) + // } + return nil + }) + }) + if err != nil { + return err + } + + return c.JSON(200, pointsResp) +} + +func (l *Location) GetLast(c echo.Context) error { + // uu := l.sessions.Get(c.Request().Context(), "user") + // if uu == nil { + // return echo.ErrForbidden + // } + // user, ok := uu.(*models.User) + // if !ok { + // return echo.ErrForbidden + // } + + lastPoint := new(models.Point) + err := l.db.View(func(tx *bbolt.Tx) error { + points := tx.Bucket([]byte("points")) + point := new(models.Point) + return points.ForEach(func(k, v []byte) error { + if err := json.Unmarshal(v, point); err != nil { + return err + } + // if slices.Equal(point.UserID, user.ID) && lastPoint.Time.Before(point.Time) { + if lastPoint.Time.Before(point.Time) { + lastPoint = point + } + return nil + }) + }) + if err != nil { + return err + } + + return c.JSON(200, lastPoint) +} + +type PointArgs struct { + Lat float64 `query:"lat"` + Lon float64 `query:"lon"` + Time time.Time `query:"time"` + Speed float64 `query:"spd"` + Direction float64 `query:"dir"` + Accuracy float64 `query:"acc"` +} diff --git a/pkg/handler/user.go b/pkg/handler/user.go new file mode 100644 index 0000000..9506c85 --- /dev/null +++ b/pkg/handler/user.go @@ -0,0 +1,155 @@ +package handler + +import ( + "encoding/json" + "net/mail" + "strings" + + "github.com/alexedwards/scs/v2" + "github.com/labstack/echo/v4" + "gitrepo.ru/neonxp/track/pkg/models" + "go.etcd.io/bbolt" + "go.neonxp.ru/objectid" + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrInvalidPasswordLen = echo.NewHTTPError(400, "Неверная длина пароля (должно быть от 8 до 32 символов)") + ErrPasswordsNotSame = echo.NewHTTPError(400, "Пароли не совпадают") + ErrInvalidEmailOrPassword = echo.NewHTTPError(400, "Неверный email или пароль") +) + +type User struct { + db *bbolt.DB + sessions *scs.SessionManager +} + +func NewUser(db *bbolt.DB, sessions *scs.SessionManager) *User { + return &User{ + db: db, + sessions: sessions, + } +} + +func (u *User) Register(c echo.Context) error { + req := new(RegisterRequest) + if err := c.Bind(req); err != nil { + return err + } + + if _, err := mail.ParseAddress(req.Email); err != nil { + return echo.NewHTTPError(400, err.Error()) + } + + if len(req.Password) < 8 || len(req.Password) > 32 { + return ErrInvalidPasswordLen + } + + if req.Password != req.Password2 { + return ErrPasswordsNotSame + } + + password, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + + user := &models.User{ + ID: objectid.New(), + Email: req.Email, + Password: password, + } + + err = u.db.Update(func(tx *bbolt.Tx) error { + users, err := tx.CreateBucketIfNotExists([]byte("users")) + if err != nil { + return err + } + + jb, err := json.Marshal(user) + if err != nil { + return err + } + + if err := users.Put([]byte(strings.ToLower(user.Email)), jb); err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + return c.JSON(201, UserResponse{ + ID: user.ID, + Email: user.Email, + }) +} + +func (u *User) Login(c echo.Context) error { + req := new(LoginRequest) + if err := c.Bind(req); err != nil { + return err + } + + user := new(models.User) + + err := u.db.View(func(tx *bbolt.Tx) error { + users := tx.Bucket([]byte("users")) + jb := users.Get([]byte(strings.ToLower(req.Email))) + if jb == nil { + return ErrInvalidEmailOrPassword + } + if err := json.Unmarshal(jb, user); err != nil { + return err + } + if err := bcrypt.CompareHashAndPassword(user.Password, []byte(req.Password)); err != nil { + return ErrInvalidEmailOrPassword + } + + return nil + }) + if err != nil { + return err + } + + u.sessions.Put(c.Request().Context(), "user", user) + + return c.JSON(200, UserResponse{ + ID: user.ID, + Email: user.Email, + }) +} + +func (u *User) User(c echo.Context) error { + uu := u.sessions.Get(c.Request().Context(), "user") + if uu == nil { + return echo.ErrForbidden + } + user, ok := uu.(*models.User) + if !ok { + return echo.ErrForbidden + } + + return c.JSON(200, UserResponse{ + ID: user.ID, + Email: user.Email, + }) +} + +type RegisterRequest struct { + Email string `json:"email"` + Password string `json:"password"` + Password2 string `json:"password2"` +} + +type LoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type UserResponse struct { + ID objectid.ID `json:"id"` + Email string `json:"email"` +} diff --git a/pkg/models/point.go b/pkg/models/point.go new file mode 100644 index 0000000..4cdcc60 --- /dev/null +++ b/pkg/models/point.go @@ -0,0 +1,18 @@ +package models + +import ( + "time" + + "go.neonxp.ru/objectid" +) + +type Point struct { + ID objectid.ID `json:"id"` + UserID objectid.ID `json:"user_id"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + Time time.Time `json:"time"` + Speed float64 `json:"speed"` + Direction float64 `json:"direction"` + Accuracy float64 `json:"accuracy"` +} diff --git a/pkg/models/user.go b/pkg/models/user.go new file mode 100644 index 0000000..5eacd7e --- /dev/null +++ b/pkg/models/user.go @@ -0,0 +1,17 @@ +package models + +import ( + "encoding/gob" + + "go.neonxp.ru/objectid" +) + +func init() { + gob.Register(new(User)) +} + +type User struct { + ID objectid.ID `json:"id"` + Email string `json:"email"` + Password []byte `json:"password"` +} |