diff --git a/Dockerfile b/Dockerfile index f89bbb32..60028422 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ FROM alpine:3.20 AS runtime # Copy the binary from the builder stage COPY --from=builder /niki/niki . -# Copy migration files +# Copy migrations files COPY --from=builder /niki/repository/mysql/migration ./repository/mysql/migration # Expose application port diff --git a/agentapp/app.go b/agentapp/app.go deleted file mode 100644 index 081c63fe..00000000 --- a/agentapp/app.go +++ /dev/null @@ -1,7 +0,0 @@ -package agentapp - -type Application struct { - config Config -} - -func Setup() {} diff --git a/agentapp/config.go b/agentapp/config.go deleted file mode 100644 index 260371db..00000000 --- a/agentapp/config.go +++ /dev/null @@ -1,7 +0,0 @@ -package agentapp - -type Config struct { - // database config - // httpserver config - //... -} diff --git a/agentapp/delivery/http/handler.go b/agentapp/delivery/http/handler.go deleted file mode 100644 index d02cfda6..00000000 --- a/agentapp/delivery/http/handler.go +++ /dev/null @@ -1 +0,0 @@ -package http diff --git a/agentapp/delivery/http/server.go b/agentapp/delivery/http/server.go deleted file mode 100644 index 3970b4cb..00000000 --- a/agentapp/delivery/http/server.go +++ /dev/null @@ -1,14 +0,0 @@ -package http - -type Server struct { - // httpServer - // handler -} - -func New() Server { - return Server{} -} - -func (s Server) Serve() {} - -func (s Server) RegisterRoutes() {} diff --git a/agentapp/repository/agent.go b/agentapp/repository/agent.go deleted file mode 100644 index 50a4378d..00000000 --- a/agentapp/repository/agent.go +++ /dev/null @@ -1 +0,0 @@ -package repository diff --git a/agentapp/service/entity.go b/agentapp/service/entity.go deleted file mode 100644 index 9a64b436..00000000 --- a/agentapp/service/entity.go +++ /dev/null @@ -1,5 +0,0 @@ -package service - -type Agent struct { - ID uint -} diff --git a/agentapp/service/param.go b/agentapp/service/param.go deleted file mode 100644 index 6d43c336..00000000 --- a/agentapp/service/param.go +++ /dev/null @@ -1 +0,0 @@ -package service diff --git a/agentapp/service/service.go b/agentapp/service/service.go deleted file mode 100644 index 160ef110..00000000 --- a/agentapp/service/service.go +++ /dev/null @@ -1,8 +0,0 @@ -package service - -type Service struct { -} - -func New() Service { - return Service{} -} diff --git a/agentapp/service/validator.go b/agentapp/service/validator.go deleted file mode 100644 index 6d43c336..00000000 --- a/agentapp/service/validator.go +++ /dev/null @@ -1 +0,0 @@ -package service diff --git a/cmd/driverapp/main.go b/cmd/driverapp/main.go new file mode 100644 index 00000000..1f1f3622 --- /dev/null +++ b/cmd/driverapp/main.go @@ -0,0 +1,52 @@ +package driverapp + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + + "git.gocasts.ir/ebhomengo/niki/driverapp" + cfgloader "git.gocasts.ir/ebhomengo/niki/pkg/cfg_loader" + "git.gocasts.ir/ebhomengo/niki/pkg/migrator" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" +) + +func main() { + var cfg driverapp.Config + + workingDir, err := os.Getwd() + if err != nil { + fmt.Printf("Error getting current working directory: %v", err) + } + + options := cfgloader.Option{ + Prefix: "DRIVER_", + Delimiter: ".", + Separator: "__", + YamlFilePath: filepath.Join(workingDir, "deploy", "driver", "development", "config.yaml"), + CallbackEnv: nil, + } + + lErr := cfgloader.Load(options, &cfg) + if lErr != nil { + log.Fatalf("Failed to load driver config: %v", err) + } + + conn := mysql.New(cfg.MysqlDB) + + mgr := migrator.New(cfg.MysqlDB, cfg.PathOfMigration) + + migrate := flag.Bool("migrate", false, "perform database migrations") + flag.Parse() + + if *migrate { + fmt.Println("Running migrations") + mgr.Up() + } + + dapp := driverapp.Setup(cfg, conn) + dapp.Start() + +} diff --git a/delivery/http_server/end2end/setup/mariadb.go b/delivery/http_server/end2end/setup/mariadb.go index 169a040e..51e6a888 100644 --- a/delivery/http_server/end2end/setup/mariadb.go +++ b/delivery/http_server/end2end/setup/mariadb.go @@ -8,7 +8,7 @@ import ( func MigrateMariaDB(cfg mysql.Config) func() { migrations := migrator.New(migrator.Config{ MysqlConfig: cfg, - MigrationPath: "../../../repository/mysql/migration", + MigrationPath: "../../../repository/mysql/migrations", MigrationDBName: "gorp_migrations", }) migrations.Up() diff --git a/deploy/driver/development/Dockerfile b/deploy/driver/development/Dockerfile new file mode 100644 index 00000000..e69de29b diff --git a/deploy/driver/development/config.yml b/deploy/driver/development/config.yml new file mode 100644 index 00000000..bae63e6e --- /dev/null +++ b/deploy/driver/development/config.yml @@ -0,0 +1,20 @@ +service: + length_of_otp_code: 6 + otp_chars: "0123456789" + otp_expire_time: 2 +redis_db: + host: + port: + password: + db: +mysql_db: + username: + password: + port: + host: + db_name: +kavenegar: + api_key: + sender: + +path_of_migration: ./driverapp/repository/mysql/migration \ No newline at end of file diff --git a/deploy/driver/development/docker-compose.yaml b/deploy/driver/development/docker-compose.yaml new file mode 100644 index 00000000..226425f4 --- /dev/null +++ b/deploy/driver/development/docker-compose.yaml @@ -0,0 +1,29 @@ +services: + driver_mariadb: + image: bitnami/mariadb:11.1 + container_name: driver_mariadb + restart: always + ports: + - "3305:3306" + volumes: + - 'driver-mariadb-data:/bitnami/mariadb' + environment: + MARIADB_USER: driver_admin + MARIADB_PASSWORD: password123 + MARIADB_DATABASE: driver_db + MARIADB_ROOT_PASSWORD: password123 + driver_redis: + image: bitnami/redis:6.2 + container_name: driver-redis + restart: always + ports: + - '6380:6379' + command: redis-server --loglevel warning --protected-mode no --save "" --appendonly no + environment: + - ALLOW_EMPTY_PASSWORD=yes + volumes: + - driver-redis-data:/data + +volumes: + driver-mariadb-data: + driver-redis-data: \ No newline at end of file diff --git a/driverapp/app.go b/driverapp/app.go new file mode 100644 index 00000000..14b554e3 --- /dev/null +++ b/driverapp/app.go @@ -0,0 +1,47 @@ +package driverapp + +import ( + "git.gocasts.ir/ebhomengo/niki/adapter/kavenegar" + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + "git.gocasts.ir/ebhomengo/niki/driverapp/delivery/http" + repo "git.gocasts.ir/ebhomengo/niki/driverapp/repository/mysql" + otp "git.gocasts.ir/ebhomengo/niki/driverapp/repository/redis" + "git.gocasts.ir/ebhomengo/niki/driverapp/service" + "git.gocasts.ir/ebhomengo/niki/pkg/http_server" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" +) + +type Application struct { + driverSvc service.Service + driverRepo repo.DriverRepo + driverRepoOtp otp.RepositoryOtp + driverHandler http.Handler + driverHttpServer http.Server + driverConfig Config +} + +func Setup(config Config, conn *mysql.DB) Application { + driverRepo := repo.New(conn) + connRedis := redis.New(config.Redis) + driverRepoOtp := otp.NewRepositoryOtp(connRedis) + kavenegarSms := kavenegar.New(config.Kavenegar) + driverValidator := service.NewValidator() + driverSvc := service.NewService(config.DriverSvc, driverRepoOtp, driverRepo, kavenegarSms, driverValidator) + driverHandler := http.NewHandler(driverSvc) + + httpServer := http_server.NewServer(config.HttpServer) + + return Application{ + driverSvc: driverSvc, + driverRepo: driverRepo, + driverRepoOtp: driverRepoOtp, + driverHandler: driverHandler, + driverHttpServer: http.New(httpServer, driverHandler), + driverConfig: config, + } + +} + +func (app Application) Start() { + app.driverHttpServer.Serve() +} diff --git a/driverapp/config.go b/driverapp/config.go new file mode 100644 index 00000000..708f1dc6 --- /dev/null +++ b/driverapp/config.go @@ -0,0 +1,18 @@ +package driverapp + +import ( + "git.gocasts.ir/ebhomengo/niki/adapter/kavenegar" + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + "git.gocasts.ir/ebhomengo/niki/driverapp/service" + "git.gocasts.ir/ebhomengo/niki/pkg/http_server" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" +) + +type Config struct { + DriverSvc service.Config `koanf:"service"` + HttpServer http_server.Config `koanf:"http_server"` + Redis redis.Config `koanf:"redis_db"` + MysqlDB mysql.Config `koanf:"mysql_db"` + Kavenegar kavenegar.Config `koanf:"kavenegar"` + PathOfMigration string `koanf:"path_of_migration"` +} diff --git a/driverapp/delivery/http/handler.go b/driverapp/delivery/http/handler.go new file mode 100644 index 00000000..ac108573 --- /dev/null +++ b/driverapp/delivery/http/handler.go @@ -0,0 +1,51 @@ +package http + +import ( + "net/http" + + "git.gocasts.ir/ebhomengo/niki/driverapp/service" + httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg" + "github.com/labstack/echo/v4" +) + +type Handler struct { + DriverSvc service.Service +} + +func NewHandler(driverSvc service.Service) Handler { + return Handler{ + DriverSvc: driverSvc, + } +} + +func (h Handler) SendOtp(c echo.Context) error { + var req service.SendOtpRequest + + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + res, err := h.DriverSvc.SendOtp(c.Request().Context(), req) + if err != nil { + msg, code := httpmsg.Error(err) + return echo.NewHTTPError(code, msg) + } + + return c.JSON(http.StatusOK, res) +} + +func (h Handler) loginOrRegister(c echo.Context) error { + var req service.LoginOrRegisterRequest + + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + res, err := h.DriverSvc.LoginOrRegister(c.Request().Context(), req) + if err != nil { + msg, code := httpmsg.Error(err) + return echo.NewHTTPError(code, msg) + } + + return c.JSON(http.StatusOK, res) +} diff --git a/driverapp/delivery/http/health_check.go b/driverapp/delivery/http/health_check.go new file mode 100644 index 00000000..b48f014e --- /dev/null +++ b/driverapp/delivery/http/health_check.go @@ -0,0 +1,13 @@ +package http + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +func (s Server) HealthCheck(c echo.Context) error { + return c.JSON(http.StatusOK, echo.Map{ + "message": "everything is good!", + }) +} diff --git a/driverapp/delivery/http/server.go b/driverapp/delivery/http/server.go new file mode 100644 index 00000000..24facef3 --- /dev/null +++ b/driverapp/delivery/http/server.go @@ -0,0 +1,36 @@ +package http + +import ( + "fmt" + + "git.gocasts.ir/ebhomengo/niki/pkg/http_server" +) + +type Server struct { + HTTPServer http_server.Server + Handler Handler +} + +func New(server http_server.Server, handler Handler) Server { + return Server{ + HTTPServer: server, + Handler: handler, + } +} + +func (s Server) Serve() { + s.RegisterRoutes() + + if err := s.HTTPServer.Start(); err != nil { + fmt.Println("router start error", err) + } +} + +func (s Server) RegisterRoutes() { + v1 := s.HTTPServer.Router.Group("/v1") + + v1.GET("/health_check", s.HealthCheck) + v1.POST("/send_otp", s.Handler.SendOtp) + v1.POST("/login_or_register", s.Handler.loginOrRegister) + +} diff --git a/driverapp/repository/mysql/db.go b/driverapp/repository/mysql/db.go new file mode 100644 index 00000000..3e2310d4 --- /dev/null +++ b/driverapp/repository/mysql/db.go @@ -0,0 +1,89 @@ +package mysql + +import ( + "context" + "database/sql" + "errors" + "time" + + "git.gocasts.ir/ebhomengo/niki/driverapp/service/entity" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/pkg/types" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" +) + +const ( + StatementKeyIsExistDriverByPhoneNumber = iota + 1 + StatementKeyCreateDriver = iota + 1 +) + +type DriverRepo struct { + conn *mysql.DB +} + +func New(conn *mysql.DB) DriverRepo { + return DriverRepo{ + conn: conn, + } +} + +func (r DriverRepo) IsExistDriverByPhoneNumber(ctx context.Context, phoneNumber string) (bool, entity.Driver, error) { + const op = "Repository.IsExistDriverByPhoneNumber" + query := `select * from drivers where phone_number = ?` + stmt, err := r.conn.PrepareStatement(ctx, StatementKeyIsExistDriverByPhoneNumber, query) + if err != nil { + return false, entity.Driver{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected). + WithMessage(errmsg.ErrorMsgCantPrepareStatement) + } + + defer stmt.Close() + + row := stmt.QueryRowContext(ctx, phoneNumber) + d, sErr := DriverScan(row) + if sErr != nil { + if errors.Is(sErr, sql.ErrNoRows) { + return false, entity.Driver{}, richerror.New(op).WithKind(richerror.KindNotFound). + WithMessage(errmsg.ErrorMsgNotFound) + } + return false, entity.Driver{}, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) + } + + return true, d, nil + +} + +func (r DriverRepo) CreateDriver(ctx context.Context, driver entity.Driver) (entity.Driver, error) { + const op = "Repository.CreateDriver" + query := `insert into drivers(phone_number) values(?)` + + stmt, err := r.conn.PrepareStatement(ctx, StatementKeyCreateDriver, query) + if err != nil { + return entity.Driver{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected). + WithMessage(errmsg.ErrorMsgCantPrepareStatement) + } + + res, err := stmt.ExecContext(ctx, driver.PhoneNumber) + + if err != nil { + return entity.Driver{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected). + WithMessage(errmsg.ErrorMsgNotFound) + } + + id, _ := res.LastInsertId() + driver.ID = types.ID(id) + + return driver, nil +} + +func DriverScan(scanner mysql.Scanner) (entity.Driver, error) { + var createdAt, updatedAt time.Time + var driver entity.Driver + + err := scanner.Scan(&driver.ID, &driver.FirstName, &driver.LastName, + &driver.PhoneNumber, &driver.NationalCode, &driver.LicenseNumber, + &driver.BirthDate, &createdAt, &updatedAt) + + return driver, err +} diff --git a/driverapp/repository/mysql/migration/1776017181_create_driver_table.sql b/driverapp/repository/mysql/migration/1776017181_create_driver_table.sql new file mode 100644 index 00000000..7b2f3663 --- /dev/null +++ b/driverapp/repository/mysql/migration/1776017181_create_driver_table.sql @@ -0,0 +1,18 @@ +-- +migrate Up +CREATE TABLE `drivers`( + `iD` INT PRIMARY KEY AUTO_INCREMENT, + `first_name` VARCHAR(191), + `last_name` VARCHAR(191), + `phone_number` VARCHAR(191) NOT NULL UNIQUE , + `national_code` VARCHAR(191) UNIQUE , + `license_number` VARCHAR(191) UNIQUE , + `birth_date` TIMESTAMP, + + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + +); + + +-- +migrate Down +DROP TABLE `drivers`; \ No newline at end of file diff --git a/driverapp/repository/redis/otp.go b/driverapp/repository/redis/otp.go new file mode 100644 index 00000000..5b4a6b15 --- /dev/null +++ b/driverapp/repository/redis/otp.go @@ -0,0 +1,67 @@ +package redis + +import ( + "context" + "time" + + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +type RepositoryOtp struct { + conn *redis.Adapter +} + +func NewRepositoryOtp(conn *redis.Adapter) RepositoryOtp { + return RepositoryOtp{conn: conn} +} + +func (r RepositoryOtp) IsExistPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) { + const op = "RepositoryOtp.IsExistPhoneNumber" + + result, err := r.conn.Client().Exists(ctx, phoneNumber).Result() + if err != nil { + return false, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) + } + + if result == 0 { + return false, nil + } + + return true, nil +} + +func (r RepositoryOtp) SaveCodeWithPhoneNumber(ctx context.Context, phoneNumber string, code string, expireTime time.Duration) error { + const op = "RepositoryOtp.SaveCodeWithPhoneNumber" + + _, err := r.conn.Client().Set(ctx, phoneNumber, code, expireTime).Result() + if err != nil { + return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) + } + + return nil +} + +func (r RepositoryOtp) GetCodeByPhoneNumber(ctx context.Context, phoneNumber string) (string, error) { + const op = "RepositoryOtp.GetCodeByPhoneNumber" + + result, err := r.conn.Client().Get(ctx, phoneNumber).Result() + if err != nil { + return "", richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) + } + + return result, nil +} + +func (r RepositoryOtp) DeleteCodeByPhoneNumber(ctx context.Context, PhoneNumber string) (bool, error) { + const op = "RepositoryOtp.DeleteCodeByPhoneNumber" + success, err := r.conn.Client().Del(ctx, PhoneNumber).Result() + if err != nil { + return false, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected) + } + if success != 1 { + return false, nil + } + + return true, nil +} diff --git a/driverapp/service/entity/driver.go b/driverapp/service/entity/driver.go new file mode 100644 index 00000000..b4b44cc9 --- /dev/null +++ b/driverapp/service/entity/driver.go @@ -0,0 +1,17 @@ +package entity + +import ( + "time" + + "git.gocasts.ir/ebhomengo/niki/pkg/types" +) + +type Driver struct { + ID types.ID + FirstName string + LastName string + PhoneNumber string + NationalCode string + LicenseNumber string + BirthDate time.Time +} diff --git a/driverapp/service/param.go b/driverapp/service/param.go new file mode 100644 index 00000000..96e7aec6 --- /dev/null +++ b/driverapp/service/param.go @@ -0,0 +1,32 @@ +package service + +import "git.gocasts.ir/ebhomengo/niki/pkg/types" + +type LoginOrRegisterRequest struct { + PhoneNumber string `json:"phone_number"` + VerifyCode string `json:"verify_code"` +} + +type LoginOrRegisterResponse struct { + Data Data `json:"data"` + Token Token `json:"token"` +} + +type Data struct { + ID types.ID `json:"id"` + PhoneNumber string `json:"phone_number"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` +} + +type Token struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +type SendOtpRequest struct { + PhoneNumber string `json:"phone_number"` +} + +type SendOtpResponse struct { +} diff --git a/driverapp/service/service.go b/driverapp/service/service.go new file mode 100644 index 00000000..4c72a7c4 --- /dev/null +++ b/driverapp/service/service.go @@ -0,0 +1,135 @@ +package service + +import ( + "context" + "math/rand" + "time" + + smscontract "git.gocasts.ir/ebhomengo/niki/contract/sms" + "git.gocasts.ir/ebhomengo/niki/driverapp/service/entity" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +type Config struct { + LengthOfOtpCode int `koanf:"length_of_otp_code"` + OtpChars string `koanf:"otp_chars"` + OtpExpireTime time.Duration `koanf:"otp_expire_time"` +} + +type RepositoryOtp 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) + DeleteCodeByPhoneNumber(ctx context.Context, PhoneNumber string) (bool, error) +} + +type Repository interface { + IsExistDriverByPhoneNumber(ctx context.Context, phoneNumber string) (bool, entity.Driver, error) + CreateDriver(ctx context.Context, driver entity.Driver) (entity.Driver, error) +} + +type Service struct { + config Config + repositoryOtp RepositoryOtp + repository Repository + smsContract smscontract.SmsAdapter + validator Validator +} + +func NewService(cfg Config, + repositoryOtp RepositoryOtp, + repository Repository, + smsContract smscontract.SmsAdapter, + validator Validator) Service { + return Service{ + config: cfg, + repositoryOtp: repositoryOtp, + repository: repository, + smsContract: smsContract, + validator: validator, + } +} + +func (s Service) SendOtp(ctx context.Context, req SendOtpRequest) (SendOtpResponse, error) { + const op = "driverService.SendOtp" + err := s.validator.ValidateSendOtpRequest(req) + if err != nil { + return SendOtpResponse{}, richerror.New(op).WithErr(err).WithMessage(err.Error()) + } + isExist, iErr := s.repositoryOtp.IsExistPhoneNumber(ctx, req.PhoneNumber) + if iErr != nil { + return SendOtpResponse{}, richerror.New(op).WithErr(iErr).WithKind(richerror.KindUnexpected) + } + + if isExist { + return SendOtpResponse{}, richerror.New(op).WithMessage(errmsg.ErrorMsgOtpCodeExist).WithKind(richerror.KindForbidden) + } + + newCode := s.generateVerificationCode() + sErr := s.repositoryOtp.SaveCodeWithPhoneNumber(ctx, req.PhoneNumber, newCode, s.config.OtpExpireTime) + if sErr != nil { + return SendOtpResponse{}, richerror.New(op).WithErr(sErr).WithKind(richerror.KindUnexpected) + } + + go s.smsContract.Send(req.PhoneNumber, newCode) + + return SendOtpResponse{}, nil +} + +func (s Service) LoginOrRegister(ctx context.Context, req LoginOrRegisterRequest) (LoginOrRegisterResponse, error) { + const op = "driverService.LoginOrRegister" + + err := s.validator.ValidateLoginOrRegisterRequest(req) + if err != nil { + return LoginOrRegisterResponse{}, richerror.New(op).WithErr(err).WithMessage(err.Error()) + } + + code, gErr := s.repositoryOtp.GetCodeByPhoneNumber(ctx, req.PhoneNumber) + if gErr != nil { + return LoginOrRegisterResponse{}, richerror.New(op).WithErr(gErr).WithKind(richerror.KindUnexpected) + } + + if code == "" || code != req.VerifyCode { + return LoginOrRegisterResponse{}, richerror.New(op).WithMessage(errmsg.ErrorMsgOtpCodeIsNotValid).WithKind(richerror.KindForbidden) + } + + _, dErr := s.repositoryOtp.DeleteCodeByPhoneNumber(ctx, req.PhoneNumber) + if dErr != nil { + return LoginOrRegisterResponse{}, richerror.New(op).WithErr(dErr).WithKind(richerror.KindUnexpected) + } + + isExist, driver, eErr := s.repository.IsExistDriverByPhoneNumber(ctx, req.PhoneNumber) + if eErr != nil { + return LoginOrRegisterResponse{}, richerror.New(op).WithErr(eErr).WithKind(richerror.KindUnexpected) + } + + if !isExist { + newDriver, cErr := s.repository.CreateDriver(ctx, entity.Driver{ + PhoneNumber: req.PhoneNumber, + }) + if cErr != nil { + return LoginOrRegisterResponse{}, richerror.New(op).WithErr(cErr).WithKind(richerror.KindUnexpected) + } + + driver = newDriver + } + + // TODO : CreateAccessToken and create CreateRefreshToken + + return LoginOrRegisterResponse{ + Data: Data{ + ID: driver.ID, + PhoneNumber: driver.PhoneNumber, + }, + }, nil +} + +func (s Service) generateVerificationCode() string { + 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/driverapp/service/validator.go b/driverapp/service/validator.go new file mode 100644 index 00000000..049ab001 --- /dev/null +++ b/driverapp/service/validator.go @@ -0,0 +1,40 @@ +package service + +import ( + "regexp" + + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + validation "github.com/go-ozzo/ozzo-validation" +) + +const ( + PhoneNumberRegex = "^(0|0098|\\+98)9(0[1-5]|[1 3]\\d|2[0-2]|98)\\d{7}$" +) + +type Validator struct{} + +func NewValidator() Validator { + return Validator{} +} + +func (v Validator) ValidateSendOtpRequest(req SendOtpRequest) error { + err := validation.ValidateStruct(&req, + validation.Field(req.PhoneNumber, + validation.Required, + validation.Match(regexp.MustCompile(PhoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid), + )) + + return err + +} + +func (v Validator) ValidateLoginOrRegisterRequest(req LoginOrRegisterRequest) error { + err := validation.ValidateStruct(&req, + validation.Field(req.PhoneNumber, + validation.Required, + validation.Match(regexp.MustCompile(PhoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid)), + validation.Field(req.VerifyCode, + validation.Required)) + + return err +} diff --git a/main.go b/main.go index 0fccecb6..eead3006 100644 --- a/main.go +++ b/main.go @@ -43,12 +43,12 @@ func Config() config.Config { } func MariaDB(cfg config.Config) *mysql.DB { - migrate := flag.Bool("migrate", false, "perform database migration") + migrate := flag.Bool("migrate", false, "perform database migrations") flag.Parse() if *migrate { migrator.New(migrator.Config{ MysqlConfig: cfg.Mysql, - MigrationPath: "./repository/mysql/migration", + MigrationPath: "./repository/mysql/migrations", MigrationDBName: "gorp_migrations", }).Up() } diff --git a/pkg/cfg_loader/cfg_loader.go b/pkg/cfg_loader/cfg_loader.go new file mode 100644 index 00000000..573b2002 --- /dev/null +++ b/pkg/cfg_loader/cfg_loader.go @@ -0,0 +1,59 @@ +package cfgloader + +import ( + "log" + "strings" + + "github.com/knadh/koanf" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/file" +) + +type Option struct { + Prefix string + Delimiter string + Separator string + YamlFilePath string + CallbackEnv func(string) string +} + +func defaultCallbackEnv(source, prefix, separator string) string { + base := strings.ToLower(strings.TrimPrefix(source, prefix)) + return strings.ReplaceAll(base, separator, ".") +} + +func Load(options Option, config interface{}) error { + k := koanf.New(options.Delimiter) + + // Load configuration from YAML file if provided + if options.YamlFilePath != "" { + if err := k.Load(file.Provider(options.YamlFilePath), yaml.Parser()); err != nil { + log.Fatalf("Error loading config file: %v", err) + return err + } + } + + callback := options.CallbackEnv + if callback == nil { + // Set default callback using the prefix and separator from options + callback = func(source string) string { + return defaultCallbackEnv(source, options.Prefix, options.Separator) + } + } + + // Load environment variables with the specified prefix and callback + if err := k.Load(env.Provider(options.Prefix, options.Delimiter, callback), nil); err != nil { + log.Fatalf("Error loading environment variables: %v", err) + return err + } + + // Unmarshal into provided config structure (passing address) + if err := k.Unmarshal("", &config); err != nil { + log.Fatalf("Error unmarshalling config: %v", err) + return err + } + + return nil + +} diff --git a/pkg/http_server/server.go b/pkg/http_server/server.go new file mode 100644 index 00000000..be2dda5a --- /dev/null +++ b/pkg/http_server/server.go @@ -0,0 +1,33 @@ +package http_server + +import ( + "fmt" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +type Config struct { + Port int `koanf:"port"` +} + +type Server struct { + Router *echo.Echo + Config Config +} + +func NewServer(cfg Config) Server { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + return Server{ + Router: e, + Config: cfg, + } +} + +func (s Server) Start() error { + return s.Router.Start(fmt.Sprintf(":%d", s.Config.Port)) +} diff --git a/pkg/migrator/migrator.go b/pkg/migrator/migrator.go new file mode 100644 index 00000000..ec975251 --- /dev/null +++ b/pkg/migrator/migrator.go @@ -0,0 +1,65 @@ +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 +} + +func New(dbConfig mysql.Config, path string) Migrator { + migrations := &migrate.FileMigrationSource{ + Dir: path, + } + + return Migrator{ + dialect: "mysql", + dbConfig: dbConfig, + migrations: migrations, + } +} + +func (m Migrator) Up() { + 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 { + fmt.Println(err) + panic(fmt.Errorf("can't open 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 db connection: %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("Rollback %d migrations!\n", n) + +} diff --git a/pkg/types/id.go b/pkg/types/id.go new file mode 100644 index 00000000..fb0c5a25 --- /dev/null +++ b/pkg/types/id.go @@ -0,0 +1,3 @@ +package types + +type ID uint64 diff --git a/repository/migrator/migrator.go b/repository/migrator/migrator.go index d9588e4b..0f76e79e 100644 --- a/repository/migrator/migrator.go +++ b/repository/migrator/migrator.go @@ -20,7 +20,7 @@ type Migrator struct { migrations *migrate.FileMigrationSource } -// TODO - set migration table name +// TODO - set migrations table name // TODO - add limit to Up and Down method func New(cfg Config) Migrator { diff --git a/repository/mysql/dbconfig.yml b/repository/mysql/dbconfig.yml index ee6c6247..5687bab9 100644 --- a/repository/mysql/dbconfig.yml +++ b/repository/mysql/dbconfig.yml @@ -1,5 +1,5 @@ production: dialect: mysql datasource: niki:nikiappt0lk2o20@(localhost:3306)/niki_db?parseTime=true - dir: repository/mysql/migration + dir: repository/mysql/migrations table: gorp_migrations