From 866c5b42e1803851b5f3287ff5af37286444bfbf Mon Sep 17 00:00:00 2001 From: masoodk Date: Sun, 14 Jan 2024 19:23:37 +0330 Subject: [PATCH] feat(benefactor): add login and register --- adapter/redis/adapter.go | 31 ++++++++ adapter/sms_provider/adapter.go | 17 ++++ adapter/sms_provider/send.go | 5 ++ config.yml | 25 +++++- config/config.go | 12 ++- config/constant.go | 15 ++++ .../benefactor/benefactor/handler.go | 24 ++++++ .../benefactor/benefactor/login_register.go | 32 ++++++++ .../benefactor/benefactor/route.go | 10 +++ .../benefactor/benefactor/send_otp.go | 32 ++++++++ .../benefactor/kind_box/handler.go | 2 +- delivery/http_server/server.go | 16 ++-- docker-compose.yaml | 34 +++++--- docker-compose.yaml.back | 37 +++++++++ entity/benefactor.go | 24 +++--- entity/benefactor_role.go | 36 +++++++++ go.mod | 6 ++ go.sum | 16 ++++ main.go | 44 +++++++++++ param/benefactor/benefactore/info.go | 8 ++ .../benefactor/benefactore/login_register.go | 11 +++ param/benefactor/benefactore/send_otp.go | 13 ++++ param/benefactor/benefactore/token.go | 6 ++ pkg/err_msg/message.go | 7 +- repository/migrator/migrator.go | 60 +++++++++++++++ repository/mysql/benefactor/create.go | 25 ++++++ repository/mysql/benefactor/db.go | 13 ++++ .../mysql/benefactor/exist_benefactor.go | 50 ++++++++++++ repository/mysql/dbconfig.yml | 5 ++ .../1705244014_add_benefactor_table.sql | 21 +++++ repository/mysql/scanner.go | 5 ++ repository/redis/redis_otp/db.go | 11 +++ .../redis/redis_otp/exist_phone_number.go | 20 +++++ repository/redis/redis_otp/get_code.go | 17 ++++ repository/redis/redis_otp/save_code.go | 17 ++++ service/auth/benefactor/claims.go | 16 ++++ service/auth/benefactor/service.go | 77 +++++++++++++++++++ service/auth/user/login.go | 1 - service/auth/user/register.go | 1 - service/auth/user/service.go | 23 ------ .../benefactor/benefactor/login_register.go | 61 +++++++++++++++ service/benefactor/benefactor/send_otp.go | 49 ++++++++++++ service/benefactor/benefactor/service.go | 53 +++++++++++++ .../benefactor/benefactor/login_register.go | 41 ++++++++++ validator/benefactor/benefactor/send_otp.go | 36 +++++++++ validator/benefactor/benefactor/validator.go | 12 +++ 46 files changed, 1016 insertions(+), 61 deletions(-) create mode 100644 adapter/redis/adapter.go create mode 100644 adapter/sms_provider/adapter.go create mode 100644 adapter/sms_provider/send.go create mode 100644 config/constant.go create mode 100644 delivery/http_server/benefactor/benefactor/handler.go create mode 100644 delivery/http_server/benefactor/benefactor/login_register.go create mode 100644 delivery/http_server/benefactor/benefactor/route.go create mode 100644 delivery/http_server/benefactor/benefactor/send_otp.go create mode 100644 docker-compose.yaml.back create mode 100644 entity/benefactor_role.go create mode 100644 param/benefactor/benefactore/info.go create mode 100644 param/benefactor/benefactore/login_register.go create mode 100644 param/benefactor/benefactore/send_otp.go create mode 100644 param/benefactor/benefactore/token.go create mode 100644 repository/migrator/migrator.go create mode 100644 repository/mysql/benefactor/create.go create mode 100644 repository/mysql/benefactor/db.go create mode 100644 repository/mysql/benefactor/exist_benefactor.go create mode 100644 repository/mysql/migration/1705244014_add_benefactor_table.sql create mode 100644 repository/mysql/scanner.go create mode 100644 repository/redis/redis_otp/db.go create mode 100644 repository/redis/redis_otp/exist_phone_number.go create mode 100644 repository/redis/redis_otp/get_code.go create mode 100644 repository/redis/redis_otp/save_code.go create mode 100644 service/auth/benefactor/claims.go create mode 100644 service/auth/benefactor/service.go delete mode 100644 service/auth/user/login.go delete mode 100644 service/auth/user/register.go delete mode 100644 service/auth/user/service.go create mode 100644 service/benefactor/benefactor/login_register.go create mode 100644 service/benefactor/benefactor/send_otp.go create mode 100644 service/benefactor/benefactor/service.go create mode 100644 validator/benefactor/benefactor/login_register.go create mode 100644 validator/benefactor/benefactor/send_otp.go create mode 100644 validator/benefactor/benefactor/validator.go diff --git a/adapter/redis/adapter.go b/adapter/redis/adapter.go new file mode 100644 index 0000000..67347c6 --- /dev/null +++ b/adapter/redis/adapter.go @@ -0,0 +1,31 @@ +package redis + +import ( + "fmt" + "github.com/redis/go-redis/v9" +) + +type Config struct { + Host string `koanf:"host"` + Port int `koanf:"port"` + Password string `koanf:"password"` + DB int `koanf:"db"` +} + +type Adapter struct { + client *redis.Client +} + +func New(config Config) Adapter { + rdb := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%d", config.Host, config.Port), + Password: config.Password, + DB: config.DB, + }) + + return Adapter{client: rdb} +} + +func (a Adapter) Client() *redis.Client { + return a.client +} diff --git a/adapter/sms_provider/adapter.go b/adapter/sms_provider/adapter.go new file mode 100644 index 0000000..200b254 --- /dev/null +++ b/adapter/sms_provider/adapter.go @@ -0,0 +1,17 @@ +package smsprovider + +type Config struct { + Host string `koanf:"host"` + Port int `koanf:"port"` +} + +type Adapter struct { +} + +func New(config Config) Adapter { + //rdb := redis.NewClient(&redis.Options{ + // Addr: fmt.Sprintf("%s:%d", config.Host, config.Port), + //}) + + return Adapter{} +} diff --git a/adapter/sms_provider/send.go b/adapter/sms_provider/send.go new file mode 100644 index 0000000..71ca6d2 --- /dev/null +++ b/adapter/sms_provider/send.go @@ -0,0 +1,5 @@ +package smsprovider + +func (a Adapter) SendSms(phoneNumber string, code string) error { + return nil +} diff --git a/config.yml b/config.yml index c4f61b0..dc69487 100644 --- a/config.yml +++ b/config.yml @@ -1,14 +1,33 @@ --- type: yml + auth: - sign_key: jwt_secret + sign_key: jwt_secret_test_nik http_server: - port: 8080 + port: 1313 mysql: port: 3308 host: localhost db_name: niki_db username: niki - password: nikit0lk2o20 + password: nikiappt0lk2o20 + +redis: + port: 6380 + host: localhost + password: "" + db: 0 + +sms_provider: + host: localhost + port: 443 + +benefactor_service: + length_of_otp_code: 5 + + + + + diff --git a/config/config.go b/config/config.go index 8349495..d9e2e36 100644 --- a/config/config.go +++ b/config/config.go @@ -1,7 +1,11 @@ package config import ( + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + smsprovider "git.gocasts.ir/ebhomengo/niki/adapter/sms_provider" "git.gocasts.ir/ebhomengo/niki/repository/mysql" + authservice "git.gocasts.ir/ebhomengo/niki/service/auth/benefactor" + benefactorservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/benefactor" ) type HTTPServer struct { @@ -9,6 +13,10 @@ type HTTPServer struct { } type Config struct { - HTTPServer HTTPServer `koanf:"http_server"` - Mysql mysql.Config `koanf:"mysql"` + HTTPServer HTTPServer `koanf:"http_server"` + Mysql mysql.Config `koanf:"mysql"` + Auth authservice.Config `koanf:"auth"` + Redis redis.Config `koanf:"redis"` + SmsProvider smsprovider.Config `koanf:"sms_provider"` + BenefactorSvc benefactorservice.Config `koanf:"benefactor_service"` } diff --git a/config/constant.go b/config/constant.go new file mode 100644 index 0000000..9209316 --- /dev/null +++ b/config/constant.go @@ -0,0 +1,15 @@ +package config + +import "time" + +const ( + OptChars = "0123456789" + OtpExpireTime time.Duration = 120000 // 2 minutes + + JwtSignKey = "jwt_secret" + AccessTokenSubject = "ac" + RefreshTokenSubject = "rt" + AccessTokenExpireDuration = time.Hour * 24 + RefreshTokenExpireDuration = time.Hour * 24 * 7 + AuthMiddlewareContextKey = "claims" +) diff --git a/delivery/http_server/benefactor/benefactor/handler.go b/delivery/http_server/benefactor/benefactor/handler.go new file mode 100644 index 0000000..14a5114 --- /dev/null +++ b/delivery/http_server/benefactor/benefactor/handler.go @@ -0,0 +1,24 @@ +package benefactorhandler + +import ( + benefactorauthservice "git.gocasts.ir/ebhomengo/niki/service/auth/benefactor" + benefactorservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/benefactor" + benefactorvalidator "git.gocasts.ir/ebhomengo/niki/validator/benefactor/benefactor" +) + +type Handler struct { + authConfig benefactorauthservice.Config + benefactorSvc benefactorservice.Service + benefactorVld benefactorvalidator.Validator +} + +func New(authConfig benefactorauthservice.Config, + benefactorSvc benefactorservice.Service, + benefactorVld benefactorvalidator.Validator, +) Handler { + return Handler{ + authConfig: authConfig, + benefactorSvc: benefactorSvc, + benefactorVld: benefactorVld, + } +} diff --git a/delivery/http_server/benefactor/benefactor/login_register.go b/delivery/http_server/benefactor/benefactor/login_register.go new file mode 100644 index 0000000..a06ec9e --- /dev/null +++ b/delivery/http_server/benefactor/benefactor/login_register.go @@ -0,0 +1,32 @@ +package benefactorhandler + +import ( + benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactore" + httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg" + "github.com/labstack/echo/v4" + "net/http" +) + +func (h Handler) loginOrRegister(c echo.Context) error { + var req benefactoreparam.LoginOrRegisterRequest + + if bErr := c.Bind(&req); bErr != nil { + return echo.NewHTTPError(http.StatusBadRequest) + } + if fieldErrors, err := h.benefactorVld.ValidateLoginRegisterRequest(req); err != nil { + msg, code := httpmsg.Error(err) + + return c.JSON(code, echo.Map{ + "message": msg, + "errors": fieldErrors, + }) + } + resp, sErr := h.benefactorSvc.LoginOrRegister(c.Request().Context(), req) + if sErr != nil { + msg, code := httpmsg.Error(sErr) + + return echo.NewHTTPError(code, msg) + } + + return c.JSON(http.StatusOK, resp) +} diff --git a/delivery/http_server/benefactor/benefactor/route.go b/delivery/http_server/benefactor/benefactor/route.go new file mode 100644 index 0000000..e53b32d --- /dev/null +++ b/delivery/http_server/benefactor/benefactor/route.go @@ -0,0 +1,10 @@ +package benefactorhandler + +import "github.com/labstack/echo/v4" + +func (h Handler) SetRoutes(e *echo.Echo) { + r := e.Group("/benefactor") + + r.POST("/send-otp", h.SendOtp) + r.POST("/login-register", h.loginOrRegister) +} diff --git a/delivery/http_server/benefactor/benefactor/send_otp.go b/delivery/http_server/benefactor/benefactor/send_otp.go new file mode 100644 index 0000000..4284aaa --- /dev/null +++ b/delivery/http_server/benefactor/benefactor/send_otp.go @@ -0,0 +1,32 @@ +package benefactorhandler + +import ( + benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactore" + httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg" + "github.com/labstack/echo/v4" + "net/http" +) + +func (h Handler) SendOtp(c echo.Context) error { + var req benefactoreparam.SendOtpRequest + + if bErr := c.Bind(&req); bErr != nil { + return echo.NewHTTPError(http.StatusBadRequest) + } + if fieldErrors, err := h.benefactorVld.ValidateSendOtpRequest(req); err != nil { + msg, code := httpmsg.Error(err) + + return c.JSON(code, echo.Map{ + "message": msg, + "errors": fieldErrors, + }) + } + resp, sErr := h.benefactorSvc.SendOtp(c.Request().Context(), req) + if sErr != nil { + msg, code := httpmsg.Error(sErr) + + return echo.NewHTTPError(code, msg) + } + + return c.JSON(http.StatusOK, resp) +} diff --git a/delivery/http_server/benefactor/kind_box/handler.go b/delivery/http_server/benefactor/kind_box/handler.go index 66da3f0..5993b78 100644 --- a/delivery/http_server/benefactor/kind_box/handler.go +++ b/delivery/http_server/benefactor/kind_box/handler.go @@ -1,7 +1,7 @@ package benefactorkindboxhandler import ( - authservice "git.gocasts.ir/ebhomengo/niki/service/auth/user" + authservice "git.gocasts.ir/ebhomengo/niki/service/auth/benefactor" benefactorkindboxservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/kind_box" benefactorkindboxvalidator "git.gocasts.ir/ebhomengo/niki/validator/benefactor/kind_box" ) diff --git a/delivery/http_server/server.go b/delivery/http_server/server.go index 797350b..2327863 100644 --- a/delivery/http_server/server.go +++ b/delivery/http_server/server.go @@ -2,6 +2,9 @@ package httpserver import ( "fmt" + benefactorhandler "git.gocasts.ir/ebhomengo/niki/delivery/http_server/benefactor/benefactor" + benefactorservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/benefactor" + benefactorvalidator "git.gocasts.ir/ebhomengo/niki/validator/benefactor/benefactor" config "git.gocasts.ir/ebhomengo/niki/config" echo "github.com/labstack/echo/v4" @@ -9,14 +12,16 @@ import ( ) type Server struct { - config config.Config - Router *echo.Echo + config config.Config + Router *echo.Echo + benefactorHandler benefactorhandler.Handler } -func New(cfg config.Config) Server { +func New(cfg config.Config, benefactorSvc benefactorservice.Service, benefactorVld benefactorvalidator.Validator) Server { return Server{ - Router: echo.New(), - config: cfg, + Router: echo.New(), + config: cfg, + benefactorHandler: benefactorhandler.New(cfg.Auth, benefactorSvc, benefactorVld), } } @@ -27,6 +32,7 @@ func (s Server) Serve() { // Routes s.Router.GET("/health-check", s.healthCheck) + s.benefactorHandler.SetRoutes(s.Router) // Start server address := fmt.Sprintf(":%d", s.config.HTTPServer.Port) diff --git a/docker-compose.yaml b/docker-compose.yaml index 56588bc..7093b28 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,19 +2,35 @@ version: '3.9' services: mysql: - platform: linux/amd64 image: mysql:8.0 ports: - - 3305:3305 + - "3308:3306" + container_name: niki-database volumes: - - ~/apps/mysql:/var/lib/mysql + - dbdata:/var/lib/mysql restart: always - hostname: mysql - container_name: niki_mysql + command: [ 'mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci' ] environment: - - MYSQL_ROOT_PASSWORD=niki_user - - MYSQL_PASSWORD=NIKI_user@123 - - MYSQL_USER=user - - MYSQL_DATABASE=niki + MYSQL_ROOT_PASSWORD: nikiRoo7t0lk2o20 + MYSQL_DATABASE: niki_db + MYSQL_USER: niki + MYSQL_PASSWORD: nikiappt0lk2o20 + + niki-redis: + image: bitnami/redis:6.2 + container_name: niki-redis + restart: always + ports: + - '6380:6379' + # TODO - remove `--save "" --appendonly no` from command to persist data + command: redis-server --loglevel warning --protected-mode no --save "" --appendonly no + environment: + - ALLOW_EMPTY_PASSWORD=yes + volumes: + - niki-redis-data:/data + +volumes: + dbdata: + niki-redis-data: diff --git a/docker-compose.yaml.back b/docker-compose.yaml.back new file mode 100644 index 0000000..e71d74a --- /dev/null +++ b/docker-compose.yaml.back @@ -0,0 +1,37 @@ +version: '3.9' + +services: + mysql: + platform: linux/amd64 + image: mysql:8.0 + ports: + - 3305:3305 + volumes: + - ~/apps/mysql:/var/lib/mysql + restart: always + hostname: mysql + container_name: niki_mysql + environment: + - MYSQL_ROOT_PASSWORD=root + - MYSQL_USER=niki_user + - MYSQL_PASSWORD=NIKI_user@123 + - MYSQL_DATABASE=niki_db + + niki-redis: + image: bitnami/redis:6.2 + container_name: niki-redis + restart: always + ports: + - '6380:6379' + # TODO - remove `--save "" --appendonly no` from command to persist data + command: redis-server --loglevel warning --protected-mode no --save "" --appendonly no + environment: + - ALLOW_EMPTY_PASSWORD=yes + volumes: + - niki-redis-data:/data + + + +volumes: + dbdata: + niki-redis-data: diff --git a/entity/benefactor.go b/entity/benefactor.go index 9bf5288..53fac3b 100644 --- a/entity/benefactor.go +++ b/entity/benefactor.go @@ -3,16 +3,16 @@ package entity import "time" type Benefactor struct { - ID uint - FirstName string - LastName string - PhoneNumber string - Address string - Description string - Email string - City string - Gender Gender - Status BenefactorStatus - Birthday time.Time - StatusChangedAt time.Time + ID uint + FirstName string + LastName string + PhoneNumber string + Address string + Description string + Email string + City string + Gender Gender + Status BenefactorStatus + Birthdate time.Time + Role UserRole } diff --git a/entity/benefactor_role.go b/entity/benefactor_role.go new file mode 100644 index 0000000..71bd9d2 --- /dev/null +++ b/entity/benefactor_role.go @@ -0,0 +1,36 @@ +package entity + +type UserRole uint + +const ( + UserBenefactorRole UserRole = iota + 1 +) + +var UserRoleStrings = map[UserRole]string{ + UserBenefactorRole: "benefactor", +} + +func (s UserRole) String() string { + return UserRoleStrings[s] +} + +// AllUserRole returns a slice containing all string values of UserRole. +func AllUserRole() []string { + roleStrings := make([]string, len(UserRoleStrings)) + for role, str := range UserRoleStrings { + roleStrings[int(role)-1] = str + } + + return roleStrings +} + +// MapToUserRole converts a string to the corresponding UserRole value. +func MapToUserRole(roleStr string) UserRole { + for role, str := range UserRoleStrings { + if str == roleStr { + return role + } + } + + return UserRole(0) +} diff --git a/go.mod b/go.mod index 0366716..c532a1c 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,20 @@ go 1.21.3 require ( github.com/go-ozzo/ozzo-validation v3.6.0+incompatible github.com/go-ozzo/ozzo-validation/v4 v4.3.0 + github.com/golang-jwt/jwt/v4 v4.5.0 github.com/knadh/koanf v1.5.0 github.com/labstack/echo/v4 v4.11.4 + github.com/redis/go-redis/v9 v9.4.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -21,6 +26,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/rubenv/sql-migrate v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/crypto v0.17.0 // indirect diff --git a/go.sum b/go.sum index 31200b2..57cf3dc 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,14 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -37,6 +43,8 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -50,6 +58,8 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -68,6 +78,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -229,8 +241,12 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk= +github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rubenv/sql-migrate v1.6.0 h1:IZpcTlAx/VKXphWEpwWJ7BaMq05tYtE80zYz+8a5Il8= +github.com/rubenv/sql-migrate v1.6.0/go.mod h1:m3ilnKP7sNb4eYkLsp6cGdPOl4OBcXM6rcbzU+Oqc5k= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= diff --git a/main.go b/main.go index da29a2c..9b53a3f 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,48 @@ package main +import ( + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + smsprovider "git.gocasts.ir/ebhomengo/niki/adapter/sms_provider" + "git.gocasts.ir/ebhomengo/niki/config" + httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server" + "git.gocasts.ir/ebhomengo/niki/repository/migrator" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" + mysqlbenefactor "git.gocasts.ir/ebhomengo/niki/repository/mysql/benefactor" + redisotp "git.gocasts.ir/ebhomengo/niki/repository/redis/redis_otp" + authservice "git.gocasts.ir/ebhomengo/niki/service/auth/benefactor" + benefactorservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/benefactor" + benefactorvalidator "git.gocasts.ir/ebhomengo/niki/validator/benefactor/benefactor" +) + func main() { + cfg := config.C() + + // TODO - add command for migrations + mgr := migrator.New(cfg.Mysql) + mgr.Up() + + _, benefactorSvc, benefactorVld := setupServices(cfg) + server := httpserver.New(cfg, benefactorSvc, benefactorVld) + server.Serve() +} + +func setupServices(cfg config.Config) ( + authservice.Service, benefactorservice.Service, benefactorvalidator.Validator, +) { + + authSvc := authservice.New(cfg.Auth) + + MysqlRepo := mysql.New(cfg.Mysql) + + redisAdapter := redis.New(cfg.Redis) + RedisOtp := redisotp.New(redisAdapter) + benefactorMysql := mysqlbenefactor.New(MysqlRepo) + smsProvider := smsprovider.New(cfg.SmsProvider) + authGenerator := authservice.New(cfg.Auth) + + benefactorSvc := benefactorservice.New(cfg.BenefactorSvc, RedisOtp, smsProvider, authGenerator, benefactorMysql) + + benefactorVld := benefactorvalidator.New() + + return authSvc, benefactorSvc, benefactorVld } diff --git a/param/benefactor/benefactore/info.go b/param/benefactor/benefactore/info.go new file mode 100644 index 0000000..19d29f1 --- /dev/null +++ b/param/benefactor/benefactore/info.go @@ -0,0 +1,8 @@ +package benefactoreparam + +type BenefactroInfo struct { + ID uint `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Role string `json:"role"` +} diff --git a/param/benefactor/benefactore/login_register.go b/param/benefactor/benefactore/login_register.go new file mode 100644 index 0000000..babcec5 --- /dev/null +++ b/param/benefactor/benefactore/login_register.go @@ -0,0 +1,11 @@ +package benefactoreparam + +type LoginOrRegisterRequest struct { + PhoneNumber string `json:"phone_number"` + VerificationCode string `json:"verification_code"` +} + +type LoginOrRegisterResponse struct { + BenefactorInfo BenefactroInfo `json:"benefactore_info"` + Tokens Tokens `json:"tokens"` +} diff --git a/param/benefactor/benefactore/send_otp.go b/param/benefactor/benefactore/send_otp.go new file mode 100644 index 0000000..946925a --- /dev/null +++ b/param/benefactor/benefactore/send_otp.go @@ -0,0 +1,13 @@ +package benefactoreparam + +type SendOtpRequest struct { + PhoneNumber string `json:"phone_number"` +} +type SendOtpResponse struct { + PhoneNumber string `json:"phone_number"` + /* + this just use in test env + TODO - remove it after test + */ + Code string `json:"code"` +} diff --git a/param/benefactor/benefactore/token.go b/param/benefactor/benefactore/token.go new file mode 100644 index 0000000..3c31348 --- /dev/null +++ b/param/benefactor/benefactore/token.go @@ -0,0 +1,6 @@ +package benefactoreparam + +type Tokens struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} diff --git a/pkg/err_msg/message.go b/pkg/err_msg/message.go index ac57851..6712767 100644 --- a/pkg/err_msg/message.go +++ b/pkg/err_msg/message.go @@ -9,8 +9,7 @@ const ( ErrorMsgPhoneNumberIsNotValid = "phone number is not valid" ErrorMsgUserNotAllowed = "user not allowed" ErrorMsgUserNotFound = "benefactor not found" + ErrorMsgOtpCodeExist = "please wait a little bit" + ErrorMsgOtpCodeIsNotValid = "verification code is not valid" + ErrorMsgCantScanQueryResult = "can't scan query result" ) - -// const ( -// ErrorMsgCantScanQueryResult = "can't scan query result" -// ) diff --git a/repository/migrator/migrator.go b/repository/migrator/migrator.go new file mode 100644 index 0000000..c07f5f7 --- /dev/null +++ b/repository/migrator/migrator.go @@ -0,0 +1,60 @@ +package migrator + +import ( + "database/sql" + "fmt" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" + migrate "github.com/rubenv/sql-migrate" +) + +type Migrator struct { + dialect string + dbConfig mysql.Config + migrations *migrate.FileMigrationSource +} + +// TODO - set migration table name +// TODO - add limit to Up and Down method + +func New(dbConfig mysql.Config) Migrator { + // OR: Read migrations from a folder: + migrations := &migrate.FileMigrationSource{ + Dir: "./repository/mysql/migrations", + } + + return Migrator{dbConfig: dbConfig, dialect: "mysql", migrations: migrations} +} + +func (m Migrator) Up() { + fmt.Println("mysql add= ", fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true", + m.dbConfig.Username, m.dbConfig.Password, m.dbConfig.Host, m.dbConfig.Port, m.dbConfig.DBName)) + db, err := sql.Open(m.dialect, fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true", + m.dbConfig.Username, m.dbConfig.Password, m.dbConfig.Host, m.dbConfig.Port, m.dbConfig.DBName)) + if err != nil { + panic(fmt.Errorf("can't open mysql db: %v", err)) + } + + n, err := migrate.Exec(db, m.dialect, m.migrations, migrate.Up) + if err != nil { + panic(fmt.Errorf("can't apply migrations: %v", err)) + } + fmt.Printf("Applied %d migrations!\n", n) +} + +func (m Migrator) Down() { + db, err := sql.Open(m.dialect, fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true", + m.dbConfig.Username, m.dbConfig.Password, m.dbConfig.Host, m.dbConfig.Port, m.dbConfig.DBName)) + if err != nil { + panic(fmt.Errorf("can't open mysql db: %v", err)) + } + + n, err := migrate.Exec(db, m.dialect, m.migrations, migrate.Down) + if err != nil { + panic(fmt.Errorf("can't rollback migrations: %v", err)) + } + fmt.Printf("Rollbacked %d migrations!\n", n) +} + +func (m Migrator) Status() { + // TODO - add status +} diff --git a/repository/mysql/benefactor/create.go b/repository/mysql/benefactor/create.go new file mode 100644 index 0000000..f94bf1c --- /dev/null +++ b/repository/mysql/benefactor/create.go @@ -0,0 +1,25 @@ +package mysqlbenefactor + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/entity" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (d DB) CreateBenefactor(ctx context.Context, benefactor entity.Benefactor) (entity.Benefactor, error) { + const op = "mysqlbenefactor.CreateBenefactor" + + res, err := d.conn.Conn().Exec(`insert into benefactors(phone_number, status, role) values(?, ?, ?)`, + benefactor.PhoneNumber, benefactor.Status.String(), benefactor.Role.String()) + if err != nil { + return entity.Benefactor{}, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgNotFound).WithKind(richerror.KindUnexpected) + } + + // error is always nil + id, _ := res.LastInsertId() + benefactor.ID = uint(id) + + return benefactor, nil +} diff --git a/repository/mysql/benefactor/db.go b/repository/mysql/benefactor/db.go new file mode 100644 index 0000000..ea4c799 --- /dev/null +++ b/repository/mysql/benefactor/db.go @@ -0,0 +1,13 @@ +package mysqlbenefactor + +import "git.gocasts.ir/ebhomengo/niki/repository/mysql" + +type DB struct { + conn *mysql.DB +} + +func New(conn *mysql.DB) *DB { + return &DB{ + conn: conn, + } +} diff --git a/repository/mysql/benefactor/exist_benefactor.go b/repository/mysql/benefactor/exist_benefactor.go new file mode 100644 index 0000000..727abf4 --- /dev/null +++ b/repository/mysql/benefactor/exist_benefactor.go @@ -0,0 +1,50 @@ +package mysqlbenefactor + +import ( + "context" + "database/sql" + "git.gocasts.ir/ebhomengo/niki/entity" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" + "time" +) + +func (d DB) IsExistBenefactorByPhoneNumber(ctx context.Context, phoneNumber string) (bool, entity.Benefactor, error) { + const op = "mysqlbenefactor.IsExistBenefactorByPhoneNumber" + + row := d.conn.Conn().QueryRowContext(ctx, `select * from benefactors where phone_number = ?`, phoneNumber) + + Benefactor, err := scanBenefactor(row) + if err != nil { + if err == sql.ErrNoRows { + return false, entity.Benefactor{}, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgNotFound).WithKind(richerror.KindNotFound) + } + + // TODO - log unexpected error for better observability + return false, entity.Benefactor{}, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) + } + + return true, Benefactor, nil +} + +func scanBenefactor(scanner mysql.Scanner) (entity.Benefactor, error) { + var createdAt time.Time + var benefactor entity.Benefactor + var roleStr, genderStr, statusStr string + + err := scanner.Scan(&benefactor.ID, &benefactor.FirstName, + &benefactor.LastName, &benefactor.PhoneNumber, + &benefactor.Address, &benefactor.Description, + &benefactor.Email, &benefactor.City, &genderStr, + &statusStr, &benefactor.Birthdate, &roleStr, + &createdAt) + + benefactor.Role = entity.MapToUserRole(roleStr) + benefactor.Gender = entity.MapToGender(genderStr) + benefactor.Status = entity.MapToBenefactorStatus(statusStr) + + return benefactor, err +} diff --git a/repository/mysql/dbconfig.yml b/repository/mysql/dbconfig.yml index e69de29..561a3f7 100644 --- a/repository/mysql/dbconfig.yml +++ b/repository/mysql/dbconfig.yml @@ -0,0 +1,5 @@ +production: + dialect: mysql + datasource: niki:nikiappt0lk2o20(localhost:3308)/niki_db?parseTime=true + dir: repository/mysql/migration + table: gorp_migrations diff --git a/repository/mysql/migration/1705244014_add_benefactor_table.sql b/repository/mysql/migration/1705244014_add_benefactor_table.sql new file mode 100644 index 0000000..05ab613 --- /dev/null +++ b/repository/mysql/migration/1705244014_add_benefactor_table.sql @@ -0,0 +1,21 @@ +-- +migrate Up +-- please read this article to understand why we use VARCHAR(191) +-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text +CREATE TABLE `benefactors` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `first_name` VARCHAR(191) , + `last_name` VARCHAR(191) , + `phone_number` VARCHAR(191) NOT NULL UNIQUE, + `address` TEXT, + `description` TEXT, + `email` VARCHAR(191), + `city` VARCHAR(191), + `gender` VARCHAR(191), + `status` VARCHAR(191), + `birthdate` TIMESTAMP, + `role` ENUM('benefactor') NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- +migrate Down +DROP TABLE `benefactors`; \ No newline at end of file diff --git a/repository/mysql/scanner.go b/repository/mysql/scanner.go new file mode 100644 index 0000000..0fe8053 --- /dev/null +++ b/repository/mysql/scanner.go @@ -0,0 +1,5 @@ +package mysql + +type Scanner interface { + Scan(dest ...any) error +} diff --git a/repository/redis/redis_otp/db.go b/repository/redis/redis_otp/db.go new file mode 100644 index 0000000..168e92c --- /dev/null +++ b/repository/redis/redis_otp/db.go @@ -0,0 +1,11 @@ +package redisotp + +import "git.gocasts.ir/ebhomengo/niki/adapter/redis" + +type DB struct { + adapter redis.Adapter +} + +func New(adapter redis.Adapter) DB { + return DB{adapter: adapter} +} diff --git a/repository/redis/redis_otp/exist_phone_number.go b/repository/redis/redis_otp/exist_phone_number.go new file mode 100644 index 0000000..a4a31c2 --- /dev/null +++ b/repository/redis/redis_otp/exist_phone_number.go @@ -0,0 +1,20 @@ +package redisotp + +import ( + "context" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (d DB) IsExistPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) { + const op = "redisotp.IsExistPhoneNumber" + + isExist, err := d.adapter.Client().Exists(ctx, phoneNumber).Result() + if err != nil { + return false, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected) + } + if isExist == 0 { + return false, nil + } + + return true, nil +} diff --git a/repository/redis/redis_otp/get_code.go b/repository/redis/redis_otp/get_code.go new file mode 100644 index 0000000..daa6c0d --- /dev/null +++ b/repository/redis/redis_otp/get_code.go @@ -0,0 +1,17 @@ +package redisotp + +import ( + "context" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (d DB) GetCodeByPhoneNumber(ctx context.Context, phoneNumber string) (string, error) { + const op = "redisotp.GetCodeByPhoneNumber" + + value, err := d.adapter.Client().Get(ctx, phoneNumber).Result() + if err != nil { + return "", richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected) + } + + return value, nil +} diff --git a/repository/redis/redis_otp/save_code.go b/repository/redis/redis_otp/save_code.go new file mode 100644 index 0000000..ef3faab --- /dev/null +++ b/repository/redis/redis_otp/save_code.go @@ -0,0 +1,17 @@ +package redisotp + +import ( + "context" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "time" +) + +func (d DB) SaveCodeWithPhoneNumber(ctx context.Context, phoneNumber string, code string, expireTime time.Duration) error { + const op = "redisotp.SaveCodeWithPhoneNumber" + err := d.adapter.Client().Set(ctx, phoneNumber, code, expireTime).Err() + if err != nil { + return richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected) + } + + return nil +} diff --git a/service/auth/benefactor/claims.go b/service/auth/benefactor/claims.go new file mode 100644 index 0000000..a57c59d --- /dev/null +++ b/service/auth/benefactor/claims.go @@ -0,0 +1,16 @@ +package benefactorauthservice + +import ( + "git.gocasts.ir/ebhomengo/niki/entity" + "github.com/golang-jwt/jwt/v4" +) + +type Claims struct { + jwt.RegisteredClaims + UserID uint `json:"user_id"` + Role entity.UserRole `json:"role"` +} + +func (c Claims) Valid() error { + return c.RegisteredClaims.Valid() +} diff --git a/service/auth/benefactor/service.go b/service/auth/benefactor/service.go new file mode 100644 index 0000000..07b390b --- /dev/null +++ b/service/auth/benefactor/service.go @@ -0,0 +1,77 @@ +package benefactorauthservice + +import ( + "git.gocasts.ir/ebhomengo/niki/entity" + "github.com/golang-jwt/jwt/v4" + "strings" + "time" +) + +type Config struct { + SignKey string `koanf:"sign_key"` + AccessExpirationTime time.Duration `koanf:"access_expiration_time"` + RefreshExpirationTime time.Duration `koanf:"refresh_expiration_time"` + AccessSubject string `koanf:"access_subject"` + RefreshSubject string `koanf:"refresh_subject"` +} + +type Service struct { + config Config +} + +func New(cfg Config) Service { + return Service{ + config: cfg, + } +} + +func (s Service) CreateAccessToken(benefactor entity.Benefactor) (string, error) { + return s.createToken(benefactor.ID, benefactor.Role, s.config.AccessSubject, s.config.AccessExpirationTime) +} + +func (s Service) CreateRefreshToken(benefactor entity.Benefactor) (string, error) { + return s.createToken(benefactor.ID, benefactor.Role, s.config.RefreshSubject, s.config.RefreshExpirationTime) +} + +func (s Service) ParseToken(bearerToken string) (*Claims, error) { + //https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-ParseWithClaims-CustomClaimsType + + tokenStr := strings.Replace(bearerToken, "Bearer ", "", 1) + + token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(s.config.SignKey), nil + }) + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } else { + return nil, err + } +} + +func (s Service) createToken(userID uint, role entity.UserRole, subject string, expireDuration time.Duration) (string, error) { + // create a signer for rsa 256 + // TODO - replace with rsa 256 RS256 - https://github.com/golang-jwt/jwt/blob/main/http_example_test.go + + // set our claims + claims := Claims{ + RegisteredClaims: jwt.RegisteredClaims{ + Subject: subject, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireDuration)), + }, + UserID: userID, + Role: role, + } + + // TODO - add sign method to config + accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := accessToken.SignedString([]byte(s.config.SignKey)) + if err != nil { + return "", err + } + + return tokenString, nil +} diff --git a/service/auth/user/login.go b/service/auth/user/login.go deleted file mode 100644 index a00006b..0000000 --- a/service/auth/user/login.go +++ /dev/null @@ -1 +0,0 @@ -package user diff --git a/service/auth/user/register.go b/service/auth/user/register.go deleted file mode 100644 index a00006b..0000000 --- a/service/auth/user/register.go +++ /dev/null @@ -1 +0,0 @@ -package user diff --git a/service/auth/user/service.go b/service/auth/user/service.go deleted file mode 100644 index 349069f..0000000 --- a/service/auth/user/service.go +++ /dev/null @@ -1,23 +0,0 @@ -package user - -import ( - "time" -) - -type Config struct { - SignKey string `koanf:"sign_key"` - AccessExpirationTime time.Duration `koanf:"access_expiration_time"` - RefreshExpirationTime time.Duration `koanf:"refresh_expiration_time"` - AccessSubject string `koanf:"access_subject"` - RefreshSubject string `koanf:"refresh_subject"` -} - -type Service struct { - config Config -} - -func New(cfg Config) Service { - return Service{ - config: cfg, - } -} diff --git a/service/benefactor/benefactor/login_register.go b/service/benefactor/benefactor/login_register.go new file mode 100644 index 0000000..2b2845a --- /dev/null +++ b/service/benefactor/benefactor/login_register.go @@ -0,0 +1,61 @@ +package benefactorservice + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/entity" + benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactore" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (s Service) LoginOrRegister(ctx context.Context, req benefactoreparam.LoginOrRegisterRequest) (benefactoreparam.LoginOrRegisterResponse, error) { + const op = "benefactorservice.LoginOrRegister" + + code, gErr := s.redisOtp.GetCodeByPhoneNumber(ctx, req.PhoneNumber) + if gErr != nil { + return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(gErr).WithKind(richerror.KindUnexpected) + } + if code == "" || code != req.VerificationCode { + return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithMessage(errmsg.ErrorMsgOtpCodeIsNotValid).WithKind(richerror.KindForbidden) + } + + isExist, benefactor, rErr := s.repo.IsExistBenefactorByPhoneNumber(ctx, req.PhoneNumber) + if rErr != nil { + return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected) + } + if !isExist { + newBenefactor, err := s.repo.CreateBenefactor(ctx, entity.Benefactor{ + PhoneNumber: "", + Status: entity.BenefactorActiveStatus, + Role: entity.UserBenefactorRole, + }) + if err != nil { + return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected) + } + + benefactor = newBenefactor + } + + accessToken, err := s.auth.CreateAccessToken(benefactor) + if err != nil { + return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected) + } + + refreshToken, err := s.auth.CreateRefreshToken(benefactor) + if err != nil { + return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected) + } + + return benefactoreparam.LoginOrRegisterResponse{ + BenefactorInfo: benefactoreparam.BenefactroInfo{ + ID: benefactor.ID, + FirstName: benefactor.FirstName, + LastName: benefactor.LastName, + Role: benefactor.Role.String(), + }, + Tokens: benefactoreparam.Tokens{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, + }, nil +} diff --git a/service/benefactor/benefactor/send_otp.go b/service/benefactor/benefactor/send_otp.go new file mode 100644 index 0000000..303ece3 --- /dev/null +++ b/service/benefactor/benefactor/send_otp.go @@ -0,0 +1,49 @@ +package benefactorservice + +import ( + "context" + benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactore" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "math/rand" + "time" +) + +func (s Service) SendOtp(ctx context.Context, req benefactoreparam.SendOtpRequest) (benefactoreparam.SendOtpResponse, error) { + const op = "benefactorservice.SendOtp" + + isExist, iErr := s.redisOtp.IsExistPhoneNumber(ctx, req.PhoneNumber) + if iErr != nil { + return benefactoreparam.SendOtpResponse{}, richerror.New(op).WithErr(iErr).WithKind(richerror.KindUnexpected) + } + if isExist { + return benefactoreparam.SendOtpResponse{}, richerror.New(op).WithMessage(errmsg.ErrorMsgOtpCodeExist).WithKind(richerror.KindForbidden) + } + + newCode := s.generateVerificationCode() + spErr := s.redisOtp.SaveCodeWithPhoneNumber(ctx, req.PhoneNumber, newCode, s.config.OtpExpireTime) + if spErr != nil { + return benefactoreparam.SendOtpResponse{}, richerror.New(op).WithErr(spErr).WithKind(richerror.KindUnexpected) + } + + //TODO- use goroutine + sErr := s.smsProviderClient.SendSms(req.PhoneNumber, newCode) + if sErr != nil { + return benefactoreparam.SendOtpResponse{}, richerror.New(op).WithErr(sErr).WithKind(richerror.KindUnexpected) + } + + // we use code in sendOtpResponse until sms provider will implement + return benefactoreparam.SendOtpResponse{ + PhoneNumber: req.PhoneNumber, + Code: newCode, //TODO - have to remove it in production + }, nil +} + +func (s Service) generateVerificationCode() string { + rand.NewSource(time.Now().UnixNano()) + result := make([]byte, s.config.LengthOfOtpCode) + for i := 0; i < s.config.LengthOfOtpCode; i++ { + result[i] = s.config.OtpChars[rand.Intn(len(s.config.OtpChars))] + } + return string(result) +} diff --git a/service/benefactor/benefactor/service.go b/service/benefactor/benefactor/service.go new file mode 100644 index 0000000..c806190 --- /dev/null +++ b/service/benefactor/benefactor/service.go @@ -0,0 +1,53 @@ +package benefactorservice + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/entity" + "time" +) + +type Config struct { + LengthOfOtpCode int `koanf:"length_of_otp_code"` + OtpChars string `koanf:"otp_chars"` + OtpExpireTime time.Duration `koanf:"otp_expire_time"` +} + +type Repository interface { + IsExistBenefactorByPhoneNumber(ctx context.Context, phoneNumber string) (bool, entity.Benefactor, error) + CreateBenefactor(ctx context.Context, benefactor entity.Benefactor) (entity.Benefactor, error) +} + +type AuthGenerator interface { + CreateAccessToken(benefactor entity.Benefactor) (string, error) + CreateRefreshToken(benefactor entity.Benefactor) (string, error) +} + +type RedisOtp interface { + IsExistPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) + SaveCodeWithPhoneNumber(ctx context.Context, phoneNumber string, code string, expireTime time.Duration) error + GetCodeByPhoneNumber(ctx context.Context, phoneNumber string) (string, error) +} + +type SmsProviderClient interface { + SendSms(phoneNumber string, code string) error +} + +type Service struct { + config Config + redisOtp RedisOtp + smsProviderClient SmsProviderClient + auth AuthGenerator + repo Repository +} + +func New(cfg Config, redisOtp RedisOtp, smsProviderClient SmsProviderClient, + auth AuthGenerator, repo Repository) Service { + + return Service{ + config: cfg, + redisOtp: redisOtp, + smsProviderClient: smsProviderClient, + auth: auth, + repo: repo, + } +} diff --git a/validator/benefactor/benefactor/login_register.go b/validator/benefactor/benefactor/login_register.go new file mode 100644 index 0000000..fbb2287 --- /dev/null +++ b/validator/benefactor/benefactor/login_register.go @@ -0,0 +1,41 @@ +package benefactorvalidator + +import ( + benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactore" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + validation "github.com/go-ozzo/ozzo-validation/v4" + + "regexp" +) + +func (v Validator) ValidateLoginRegisterRequest(req benefactoreparam.LoginOrRegisterRequest) (map[string]string, error) { + const op = "benefactorvalidator.ValidateRegisterRequest" + + if err := validation.ValidateStruct(&req, + // TODO - add length of code config from benefactor config + //validation.Field(&req.VerificationCode, + // validation.Required, + // validation.Length(3, 50)), + + validation.Field(&req.PhoneNumber, + validation.Required, + validation.Match(regexp.MustCompile(phoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid))); err != nil { + fieldErrors := make(map[string]string) + + errV, ok := err.(validation.Errors) + if ok { + for key, value := range errV { + if value != nil { + fieldErrors[key] = value.Error() + } + } + } + + return fieldErrors, richerror.New(op).WithMessage(errmsg.ErrorMsgInvalidInput). + WithKind(richerror.KindInvalid). + WithMeta(map[string]interface{}{"req": req}).WithErr(err) + } + + return nil, nil +} diff --git a/validator/benefactor/benefactor/send_otp.go b/validator/benefactor/benefactor/send_otp.go new file mode 100644 index 0000000..71c8116 --- /dev/null +++ b/validator/benefactor/benefactor/send_otp.go @@ -0,0 +1,36 @@ +package benefactorvalidator + +import ( + benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactore" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + validation "github.com/go-ozzo/ozzo-validation/v4" + + "regexp" +) + +func (v Validator) ValidateSendOtpRequest(req benefactoreparam.SendOtpRequest) (map[string]string, error) { + const op = "benefactorvalidator.ValidateSendOtpRequest" + + if err := validation.ValidateStruct(&req, + validation.Field(&req.PhoneNumber, + validation.Required, + validation.Match(regexp.MustCompile(phoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid))); err != nil { + fieldErrors := make(map[string]string) + + errV, ok := err.(validation.Errors) + if ok { + for key, value := range errV { + if value != nil { + fieldErrors[key] = value.Error() + } + } + } + + return fieldErrors, richerror.New(op).WithMessage(errmsg.ErrorMsgInvalidInput). + WithKind(richerror.KindInvalid). + WithMeta(map[string]interface{}{"req": req}).WithErr(err) + } + + return nil, nil +} diff --git a/validator/benefactor/benefactor/validator.go b/validator/benefactor/benefactor/validator.go new file mode 100644 index 0000000..7101172 --- /dev/null +++ b/validator/benefactor/benefactor/validator.go @@ -0,0 +1,12 @@ +package benefactorvalidator + +const ( + phoneNumberRegex = "^09[0-9]{9}$" +) + +type Validator struct { +} + +func New() Validator { + return Validator{} +}