forked from ebhomengo/niki
1
0
Fork 0

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 {
@ -11,4 +15,8 @@ type HTTPServer struct {
type Config struct {
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"
@ -11,12 +14,14 @@ import (
type Server struct {
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,
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

@ -13,6 +13,6 @@ type Benefactor struct {
City string
Gender Gender
Status BenefactorStatus
Birthday time.Time
StatusChangedAt time.Time
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{}
}