summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/app/app.go90
-rw-r--r--pkg/config/config.go16
-rw-r--r--pkg/handler/location.go141
-rw-r--r--pkg/handler/user.go155
-rw-r--r--pkg/models/point.go18
-rw-r--r--pkg/models/user.go17
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"`
+}