feat(benefactor): add login and register

This commit is contained in:
Masood Keshvari 2024-01-14 19:23:37 +03:30
parent 052f062cca
commit 866c5b42e1
46 changed files with 1016 additions and 61 deletions

31
adapter/redis/adapter.go Normal file
View File

@ -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
}

View File

@ -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{}
}

View File

@ -0,0 +1,5 @@
package smsprovider
func (a Adapter) SendSms(phoneNumber string, code string) error {
return nil
}

View File

@ -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

View File

@ -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"`
}

15
config/constant.go Normal file
View File

@ -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"
)

View File

@ -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,
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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"
)

View File

@ -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)

View File

@ -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:

37
docker-compose.yaml.back Normal file
View File

@ -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:

View File

@ -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
}

36
entity/benefactor_role.go Normal file
View File

@ -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)
}

6
go.mod
View File

@ -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

16
go.sum
View File

@ -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=

44
main.go
View File

@ -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
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -0,0 +1,6 @@
package benefactoreparam
type Tokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}

View File

@ -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"
// )

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -0,0 +1,5 @@
production:
dialect: mysql
datasource: niki:nikiappt0lk2o20(localhost:3308)/niki_db?parseTime=true
dir: repository/mysql/migration
table: gorp_migrations

View File

@ -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`;

View File

@ -0,0 +1,5 @@
package mysql
type Scanner interface {
Scan(dest ...any) error
}

View File

@ -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}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -1 +0,0 @@
package user

View File

@ -1 +0,0 @@
package user

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,12 @@
package benefactorvalidator
const (
phoneNumberRegex = "^09[0-9]{9}$"
)
type Validator struct {
}
func New() Validator {
return Validator{}
}