forked from ebhomengo/niki
feat(driver)impl logic send_otp and loginOrRegister
This commit is contained in:
parent
a193abeb56
commit
00fb0a7ead
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
package agentapp
|
||||
|
||||
type Application struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
func Setup() {}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package agentapp
|
||||
|
||||
type Config struct {
|
||||
// database config
|
||||
// httpserver config
|
||||
//...
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package http
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package http
|
||||
|
||||
type Server struct {
|
||||
// httpServer
|
||||
// handler
|
||||
}
|
||||
|
||||
func New() Server {
|
||||
return Server{}
|
||||
}
|
||||
|
||||
func (s Server) Serve() {}
|
||||
|
||||
func (s Server) RegisterRoutes() {}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package repository
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package service
|
||||
|
||||
type Agent struct {
|
||||
ID uint
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package service
|
||||
|
||||
type Service struct {
|
||||
}
|
||||
|
||||
func New() Service {
|
||||
return Service{}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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!",
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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`;
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
4
main.go
4
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package types
|
||||
|
||||
type ID uint64
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue