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/accountapp/app.go b/accountapp/app.go new file mode 100644 index 00000000..0d175053 --- /dev/null +++ b/accountapp/app.go @@ -0,0 +1,44 @@ +package accountapp + +import ( + "log" + + "git.gocasts.ir/ebhomengo/niki/accountapp/delivery/grpc" + "git.gocasts.ir/ebhomengo/niki/adapter/kavenegar" + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + "git.gocasts.ir/ebhomengo/niki/domain/account/repository/mysql" + redisRepo "git.gocasts.ir/ebhomengo/niki/domain/account/repository/redis" + "git.gocasts.ir/ebhomengo/niki/domain/account/service" + database "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql" + rpcPkg "git.gocasts.ir/ebhomengo/niki/pkg/grpc" +) + +type Application struct { + GrpcServer grpc.Server + Config Config + accountSvc service.Service +} + +func Setup(cfg Config, db *database.DB) Application { + redisConn := redis.New(cfg.Redis) + otpRepo := redisRepo.NewRepositoryOtp(redisConn) + mysqlRepo := mysql.New(db) + smsAdapter := kavenegar.New(cfg.Kavenegar) + accountSvc := service.NewService(cfg.accountSvc, otpRepo, mysqlRepo, smsAdapter) + + rpcServer := rpcPkg.New(cfg.grpcServerCfg) + + return Application{ + accountSvc: accountSvc, + Config: cfg, + GrpcServer: grpc.New(rpcServer, accountSvc), + } +} + +func (app *Application) Start() { + err := app.GrpcServer.Start() + if err != nil { + log.Fatalf("error in serving GRPC server: %v", err) + } + +} diff --git a/accountapp/config.go b/accountapp/config.go new file mode 100644 index 00000000..25361b7f --- /dev/null +++ b/accountapp/config.go @@ -0,0 +1,19 @@ +package accountapp + +import ( + "git.gocasts.ir/ebhomengo/niki/adapter/kavenegar" + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + "git.gocasts.ir/ebhomengo/niki/domain/account/service" + "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql" + "git.gocasts.ir/ebhomengo/niki/pkg/grpc" +) + +type Config struct { + accountSvc service.Config `koanf:"service"` + Redis redis.Config `koanf:"redis_db"` + MysqlDB mysql.Config `koanf:"mysql_db"` + Kavenegar kavenegar.Config `koanf:"kavenegar"` + grpcServerCfg grpc.Config `koanf:"grpc_server"` + grpcClientCfg grpc.Client `koanf:"grpc_client"` + PathOfMigration string `koanf:"path_of_migration"` +} diff --git a/accountapp/delivery/grpc/server.go b/accountapp/delivery/grpc/server.go new file mode 100644 index 00000000..3f68a27a --- /dev/null +++ b/accountapp/delivery/grpc/server.go @@ -0,0 +1,68 @@ +package grpc + +import ( + "context" + "fmt" + "log" + "net" + + pb "git.gocasts.ir/ebhomengo/niki/contract/goprotobuf/account" + "git.gocasts.ir/ebhomengo/niki/domain/account/service" + "git.gocasts.ir/ebhomengo/niki/pkg/grpc" +) + +type Server struct { + server *grpc.RPCServer + accountSvc service.Service + pb.UnimplementedAccountServiceServer +} + +func New(server *grpc.RPCServer, accountSvc service.Service) Server { + return Server{ + server: server, + accountSvc: accountSvc, + } +} + +func (s Server) SendOtp(ctx context.Context, req *pb.SendOtpRequest) (*pb.SendOtpResponse, error) { + err := s.accountSvc.SendOTP(ctx, req.PhoneNumber) + if err != nil { + return nil, err + } + + return &pb.SendOtpResponse{}, nil +} + +func (s Server) LoginOrRegister(ctx context.Context, req *pb.LoginOrRegisterRequest) (*pb.LoginOrRegisterResponse, error) { + res := &pb.LoginOrRegisterResponse{} + driver, err := s.accountSvc.LoginOrRegisterDriver(ctx, req.PhoneNumber, req.VerifyCode) + if err != nil { + return nil, err + } + + id := uint64(driver.ID) + + res.Id = id + res.PhoneNumber = driver.PhoneNumber + + return res, nil + +} + +func (s Server) Start() error { + listener, err := net.Listen(s.server.Config.NetworkType, fmt.Sprintf(":%d", s.server.Config.Port)) + + if err != nil { + return err + } + + accountSvcServer := Server{} + + pb.RegisterAccountServiceServer(s.server.Server, &accountSvcServer) + + if err := s.server.Server.Serve(listener); err != nil { + log.Fatalf("failed to serve: %v", err) + } + + return nil +} diff --git a/adapter/account/client.go b/adapter/account/client.go new file mode 100644 index 00000000..87d7aa6d --- /dev/null +++ b/adapter/account/client.go @@ -0,0 +1,53 @@ +package account + +import ( + "context" + + pb "git.gocasts.ir/ebhomengo/niki/contract/goprotobuf/account" + "git.gocasts.ir/ebhomengo/niki/driverapp/service" + "git.gocasts.ir/ebhomengo/niki/pkg/types" + + "google.golang.org/grpc" +) + +type Client struct { + Conn *grpc.ClientConn +} + +func New(conn *grpc.ClientConn) *Client { + return &Client{ + Conn: conn, + } +} + +func (c Client) SendOTP(ctx context.Context, phoneNumber string) error { + + client := pb.NewAccountServiceClient(c.Conn) + + _, err := client.SendOtp(ctx, &pb.SendOtpRequest{ + PhoneNumber: phoneNumber, + }) + if err != nil { + return err + } + + return nil +} + +func (c Client) LoginOrRegister(ctx context.Context, req service.LoginOrRegisterRequest) (service.LoginOrRegisterResponse, error) { + + client := pb.NewAccountServiceClient(c.Conn) + + res, err := client.LoginOrRegister(ctx, &pb.LoginOrRegisterRequest{ + PhoneNumber: req.PhoneNumber, + VerifyCode: req.VerifyCode, + }) + if err != nil { + return service.LoginOrRegisterResponse{}, err + } + + return service.LoginOrRegisterResponse{ + ID: types.ID(res.Id), + PhoneNumber: res.PhoneNumber, + }, nil +} 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/account/command/migrate.go b/cmd/account/command/migrate.go new file mode 100644 index 00000000..ea657c42 --- /dev/null +++ b/cmd/account/command/migrate.go @@ -0,0 +1,23 @@ +package command + +import "github.com/spf13/cobra" + +var up bool +var down bool + +var migrateCmd = &cobra.Command{ + Use: "migrate", + Short: "Run database migrations", + Long: `This command runs the database migrations for the account service.`, + Run: func(cmd *cobra.Command, args []string) { + migrate() + }, +} + +func migrate() {} + +func init() { + migrateCmd.Flags().BoolVar(&up, "up", false, "Run migrations up") + migrateCmd.Flags().BoolVar(&down, "down", false, "Run migrations down") + RootCmd.AddCommand(migrateCmd) +} diff --git a/cmd/account/command/root.go b/cmd/account/command/root.go new file mode 100644 index 00000000..f5fa0806 --- /dev/null +++ b/cmd/account/command/root.go @@ -0,0 +1,10 @@ +package command + +import "github.com/spf13/cobra" + +var RootCmd = &cobra.Command{ + Use: "account_service", + Short: "A CLI for account Service", + Long: `account Service CLI is a tool to manage and run +the account service, including migrations and server startup.`, +} diff --git a/cmd/account/command/serve.go b/cmd/account/command/serve.go new file mode 100644 index 00000000..6f8e40a1 --- /dev/null +++ b/cmd/account/command/serve.go @@ -0,0 +1,14 @@ +package command + +import "github.com/spf13/cobra" + +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "start a account service.", + Long: `This command starts the main account service.`, + Run: func(cmd *cobra.Command, args []string) { + serve() + }, +} + +func serve() {} diff --git a/cmd/account/main.go b/cmd/account/main.go new file mode 100644 index 00000000..75fed715 --- /dev/null +++ b/cmd/account/main.go @@ -0,0 +1,14 @@ +package account + +import ( + "os" + + "git.gocasts.ir/ebhomengo/niki/cmd/account/command" +) + +func main() { + if err := command.RootCmd.Execute(); err != nil { + os.Exit(1) + } + +} diff --git a/cmd/driverapp/main.go b/cmd/driverapp/main.go new file mode 100644 index 00000000..6e073c5b --- /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) + //dapp.Start() + +} diff --git a/contract/goprotobuf/account/account.pb.go b/contract/goprotobuf/account/account.pb.go new file mode 100644 index 00000000..33ecaf38 --- /dev/null +++ b/contract/goprotobuf/account/account.pb.go @@ -0,0 +1,281 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.21.12 +// source: contract/protobuf/account/account.proto + +package account + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type LoginOrRegisterRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PhoneNumber string `protobuf:"bytes,1,opt,name=phoneNumber,proto3" json:"phoneNumber,omitempty"` + VerifyCode string `protobuf:"bytes,2,opt,name=verifyCode,proto3" json:"verifyCode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoginOrRegisterRequest) Reset() { + *x = LoginOrRegisterRequest{} + mi := &file_contract_protobuf_account_account_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginOrRegisterRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginOrRegisterRequest) ProtoMessage() {} + +func (x *LoginOrRegisterRequest) ProtoReflect() protoreflect.Message { + mi := &file_contract_protobuf_account_account_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginOrRegisterRequest.ProtoReflect.Descriptor instead. +func (*LoginOrRegisterRequest) Descriptor() ([]byte, []int) { + return file_contract_protobuf_account_account_proto_rawDescGZIP(), []int{0} +} + +func (x *LoginOrRegisterRequest) GetPhoneNumber() string { + if x != nil { + return x.PhoneNumber + } + return "" +} + +func (x *LoginOrRegisterRequest) GetVerifyCode() string { + if x != nil { + return x.VerifyCode + } + return "" +} + +type LoginOrRegisterResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + PhoneNumber string `protobuf:"bytes,2,opt,name=phoneNumber,proto3" json:"phoneNumber,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoginOrRegisterResponse) Reset() { + *x = LoginOrRegisterResponse{} + mi := &file_contract_protobuf_account_account_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginOrRegisterResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginOrRegisterResponse) ProtoMessage() {} + +func (x *LoginOrRegisterResponse) ProtoReflect() protoreflect.Message { + mi := &file_contract_protobuf_account_account_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginOrRegisterResponse.ProtoReflect.Descriptor instead. +func (*LoginOrRegisterResponse) Descriptor() ([]byte, []int) { + return file_contract_protobuf_account_account_proto_rawDescGZIP(), []int{1} +} + +func (x *LoginOrRegisterResponse) GetId() uint64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *LoginOrRegisterResponse) GetPhoneNumber() string { + if x != nil { + return x.PhoneNumber + } + return "" +} + +type SendOtpRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PhoneNumber string `protobuf:"bytes,1,opt,name=phoneNumber,proto3" json:"phoneNumber,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendOtpRequest) Reset() { + *x = SendOtpRequest{} + mi := &file_contract_protobuf_account_account_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendOtpRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendOtpRequest) ProtoMessage() {} + +func (x *SendOtpRequest) ProtoReflect() protoreflect.Message { + mi := &file_contract_protobuf_account_account_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendOtpRequest.ProtoReflect.Descriptor instead. +func (*SendOtpRequest) Descriptor() ([]byte, []int) { + return file_contract_protobuf_account_account_proto_rawDescGZIP(), []int{2} +} + +func (x *SendOtpRequest) GetPhoneNumber() string { + if x != nil { + return x.PhoneNumber + } + return "" +} + +type SendOtpResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendOtpResponse) Reset() { + *x = SendOtpResponse{} + mi := &file_contract_protobuf_account_account_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendOtpResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendOtpResponse) ProtoMessage() {} + +func (x *SendOtpResponse) ProtoReflect() protoreflect.Message { + mi := &file_contract_protobuf_account_account_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendOtpResponse.ProtoReflect.Descriptor instead. +func (*SendOtpResponse) Descriptor() ([]byte, []int) { + return file_contract_protobuf_account_account_proto_rawDescGZIP(), []int{3} +} + +var File_contract_protobuf_account_account_proto protoreflect.FileDescriptor + +const file_contract_protobuf_account_account_proto_rawDesc = "" + + "\n" + + "'contract/protobuf/account/account.proto\x12\asendOtp\"Z\n" + + "\x16LoginOrRegisterRequest\x12 \n" + + "\vphoneNumber\x18\x01 \x01(\tR\vphoneNumber\x12\x1e\n" + + "\n" + + "verifyCode\x18\x02 \x01(\tR\n" + + "verifyCode\"K\n" + + "\x17LoginOrRegisterResponse\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x04R\x02id\x12 \n" + + "\vphoneNumber\x18\x02 \x01(\tR\vphoneNumber\"2\n" + + "\x0eSendOtpRequest\x12 \n" + + "\vphoneNumber\x18\x01 \x01(\tR\vphoneNumber\"\x11\n" + + "\x0fSendOtpResponse2\xa4\x01\n" + + "\x0eAccountService\x12<\n" + + "\aSendOtp\x12\x17.sendOtp.SendOtpRequest\x1a\x18.sendOtp.SendOtpResponse\x12T\n" + + "\x0fLoginOrRegister\x12\x1f.sendOtp.LoginOrRegisterRequest\x1a .sendOtp.LoginOrRegisterResponseB\x1dZ\x1bcontract/goprotobuf/accountb\x06proto3" + +var ( + file_contract_protobuf_account_account_proto_rawDescOnce sync.Once + file_contract_protobuf_account_account_proto_rawDescData []byte +) + +func file_contract_protobuf_account_account_proto_rawDescGZIP() []byte { + file_contract_protobuf_account_account_proto_rawDescOnce.Do(func() { + file_contract_protobuf_account_account_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_contract_protobuf_account_account_proto_rawDesc), len(file_contract_protobuf_account_account_proto_rawDesc))) + }) + return file_contract_protobuf_account_account_proto_rawDescData +} + +var file_contract_protobuf_account_account_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_contract_protobuf_account_account_proto_goTypes = []any{ + (*LoginOrRegisterRequest)(nil), // 0: sendOtp.LoginOrRegisterRequest + (*LoginOrRegisterResponse)(nil), // 1: sendOtp.LoginOrRegisterResponse + (*SendOtpRequest)(nil), // 2: sendOtp.SendOtpRequest + (*SendOtpResponse)(nil), // 3: sendOtp.SendOtpResponse +} +var file_contract_protobuf_account_account_proto_depIdxs = []int32{ + 2, // 0: sendOtp.AccountService.SendOtp:input_type -> sendOtp.SendOtpRequest + 0, // 1: sendOtp.AccountService.LoginOrRegister:input_type -> sendOtp.LoginOrRegisterRequest + 3, // 2: sendOtp.AccountService.SendOtp:output_type -> sendOtp.SendOtpResponse + 1, // 3: sendOtp.AccountService.LoginOrRegister:output_type -> sendOtp.LoginOrRegisterResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_contract_protobuf_account_account_proto_init() } +func file_contract_protobuf_account_account_proto_init() { + if File_contract_protobuf_account_account_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_contract_protobuf_account_account_proto_rawDesc), len(file_contract_protobuf_account_account_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_contract_protobuf_account_account_proto_goTypes, + DependencyIndexes: file_contract_protobuf_account_account_proto_depIdxs, + MessageInfos: file_contract_protobuf_account_account_proto_msgTypes, + }.Build() + File_contract_protobuf_account_account_proto = out.File + file_contract_protobuf_account_account_proto_goTypes = nil + file_contract_protobuf_account_account_proto_depIdxs = nil +} diff --git a/contract/goprotobuf/account/account_grpc.pb.go b/contract/goprotobuf/account/account_grpc.pb.go new file mode 100644 index 00000000..bdd4c719 --- /dev/null +++ b/contract/goprotobuf/account/account_grpc.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v3.21.12 +// source: contract/protobuf/account/account.proto + +package account + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AccountService_SendOtp_FullMethodName = "/sendOtp.AccountService/SendOtp" + AccountService_LoginOrRegister_FullMethodName = "/sendOtp.AccountService/LoginOrRegister" +) + +// AccountServiceClient is the client API for AccountService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AccountServiceClient interface { + SendOtp(ctx context.Context, in *SendOtpRequest, opts ...grpc.CallOption) (*SendOtpResponse, error) + LoginOrRegister(ctx context.Context, in *LoginOrRegisterRequest, opts ...grpc.CallOption) (*LoginOrRegisterResponse, error) +} + +type accountServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAccountServiceClient(cc grpc.ClientConnInterface) AccountServiceClient { + return &accountServiceClient{cc} +} + +func (c *accountServiceClient) SendOtp(ctx context.Context, in *SendOtpRequest, opts ...grpc.CallOption) (*SendOtpResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SendOtpResponse) + err := c.cc.Invoke(ctx, AccountService_SendOtp_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *accountServiceClient) LoginOrRegister(ctx context.Context, in *LoginOrRegisterRequest, opts ...grpc.CallOption) (*LoginOrRegisterResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LoginOrRegisterResponse) + err := c.cc.Invoke(ctx, AccountService_LoginOrRegister_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AccountServiceServer is the server API for AccountService service. +// All implementations must embed UnimplementedAccountServiceServer +// for forward compatibility. +type AccountServiceServer interface { + SendOtp(context.Context, *SendOtpRequest) (*SendOtpResponse, error) + LoginOrRegister(context.Context, *LoginOrRegisterRequest) (*LoginOrRegisterResponse, error) + mustEmbedUnimplementedAccountServiceServer() +} + +// UnimplementedAccountServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAccountServiceServer struct{} + +func (UnimplementedAccountServiceServer) SendOtp(context.Context, *SendOtpRequest) (*SendOtpResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SendOtp not implemented") +} +func (UnimplementedAccountServiceServer) LoginOrRegister(context.Context, *LoginOrRegisterRequest) (*LoginOrRegisterResponse, error) { + return nil, status.Error(codes.Unimplemented, "method LoginOrRegister not implemented") +} +func (UnimplementedAccountServiceServer) mustEmbedUnimplementedAccountServiceServer() {} +func (UnimplementedAccountServiceServer) testEmbeddedByValue() {} + +// UnsafeAccountServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AccountServiceServer will +// result in compilation errors. +type UnsafeAccountServiceServer interface { + mustEmbedUnimplementedAccountServiceServer() +} + +func RegisterAccountServiceServer(s grpc.ServiceRegistrar, srv AccountServiceServer) { + // If the following call panics, it indicates UnimplementedAccountServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AccountService_ServiceDesc, srv) +} + +func _AccountService_SendOtp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendOtpRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServiceServer).SendOtp(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AccountService_SendOtp_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServiceServer).SendOtp(ctx, req.(*SendOtpRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AccountService_LoginOrRegister_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoginOrRegisterRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServiceServer).LoginOrRegister(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AccountService_LoginOrRegister_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServiceServer).LoginOrRegister(ctx, req.(*LoginOrRegisterRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AccountService_ServiceDesc is the grpc.ServiceDesc for AccountService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AccountService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "sendOtp.AccountService", + HandlerType: (*AccountServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SendOtp", + Handler: _AccountService_SendOtp_Handler, + }, + { + MethodName: "LoginOrRegister", + Handler: _AccountService_LoginOrRegister_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "contract/protobuf/account/account.proto", +} diff --git a/contract/protobuf/account/account.proto b/contract/protobuf/account/account.proto new file mode 100644 index 00000000..e58f1413 --- /dev/null +++ b/contract/protobuf/account/account.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + + +package sendOtp; +option go_package = "contract/goprotobuf/account"; + +message LoginOrRegisterRequest{ + string phoneNumber = 1; + string verifyCode = 2; +} + +message LoginOrRegisterResponse { + uint64 id = 1; + string phoneNumber = 2; +} + + +message SendOtpRequest { + string phoneNumber = 1; +} + +message SendOtpResponse {} + +service AccountService { + rpc SendOtp(SendOtpRequest) returns (SendOtpResponse); + rpc LoginOrRegister(LoginOrRegisterRequest) returns(LoginOrRegisterResponse); +} + 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/account/development/config.yml b/deploy/account/development/config.yml new file mode 100644 index 00000000..bbb8e845 --- /dev/null +++ b/deploy/account/development/config.yml @@ -0,0 +1,27 @@ +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: +grpc_server: + port: + network: +grpc_client: + host: + port: + + +path_of_migration: ./account/repository/mysql/migration \ No newline at end of file diff --git a/deploy/account/development/docker-compose.yml b/deploy/account/development/docker-compose.yml new file mode 100644 index 00000000..e69de29b 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/domain/account/entity/driver.go b/domain/account/entity/driver.go new file mode 100644 index 00000000..5d5ca041 --- /dev/null +++ b/domain/account/entity/driver.go @@ -0,0 +1,8 @@ +package entity + +import "git.gocasts.ir/ebhomengo/niki/pkg/types" + +type Driver struct { + ID types.ID + PhoneNumber string +} diff --git a/domain/account/repository/mysql/db.go b/domain/account/repository/mysql/db.go new file mode 100644 index 00000000..4f220159 --- /dev/null +++ b/domain/account/repository/mysql/db.go @@ -0,0 +1,87 @@ +package mysql + +import ( + "context" + "database/sql" + "errors" + "time" + + "git.gocasts.ir/ebhomengo/niki/domain/account/entity" + "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + types "git.gocasts.ir/ebhomengo/niki/pkg/types" +) + +const ( + StatementKeyIsExistDriverByPhoneNumber = iota + 1 + StatementKeyCreateDriver = iota + 1 +) + +type AccountRepo struct { + db *mysql.DB +} + +func New(db *mysql.DB) AccountRepo { + return AccountRepo{ + db: db, + } +} + +func (r AccountRepo) IsExistDriverByPhoneNumber(ctx context.Context, phoneNumber string) (bool, entity.Driver, error) { + const op = "Repository.IsExistDriverByPhoneNumber" + query := `select * from drivers where phone_number = ?` + stmt, err := r.db.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 AccountRepo) CreateDriver(ctx context.Context, driver entity.Driver) (entity.Driver, error) { + const op = "Repository.CreateDriver" + query := `insert into drivers(phone_number) values(?)` + + stmt, err := r.db.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.PhoneNumber, &createdAt, &updatedAt) + + return driver, err +} diff --git a/domain/account/repository/mysql/migration/1776733883_create_driver_table.sql b/domain/account/repository/mysql/migration/1776733883_create_driver_table.sql new file mode 100644 index 00000000..d725e3f2 --- /dev/null +++ b/domain/account/repository/mysql/migration/1776733883_create_driver_table.sql @@ -0,0 +1,13 @@ +-- +migrate Up +CREATE TABLE `drivers`( + `iD` INT PRIMARY KEY AUTO_INCREMENT, + `phone_number` VARCHAR(191) NOT NULL UNIQUE , + + `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/domain/account/repository/redis/otp.go b/domain/account/repository/redis/otp.go new file mode 100644 index 00000000..5b4a6b15 --- /dev/null +++ b/domain/account/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/domain/account/service/service.go b/domain/account/service/service.go new file mode 100644 index 00000000..09ba4ce1 --- /dev/null +++ b/domain/account/service/service.go @@ -0,0 +1,115 @@ +package service + +import ( + "context" + "math/rand" + "time" + + smscontract "git.gocasts.ir/ebhomengo/niki/contract/sms" + "git.gocasts.ir/ebhomengo/niki/domain/account/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 +} + +func NewService(cfg Config, repositoryOtp RepositoryOtp, repository Repository, smsContract smscontract.SmsAdapter) Service { + return Service{ + config: cfg, + repositoryOtp: repositoryOtp, + repository: repository, + smsContract: smsContract, + } +} + +func (s Service) SendOTP(ctx context.Context, phoneNumber string) error { + const op = "accountService.SendOTP" + + isExist, iErr := s.repositoryOtp.IsExistPhoneNumber(ctx, phoneNumber) + if iErr != nil { + return richerror.New(op).WithErr(iErr).WithKind(richerror.KindUnexpected) + } + + if isExist { + return richerror.New(op).WithMessage(errmsg.ErrorMsgOtpCodeExist).WithKind(richerror.KindForbidden) + } + + newCode := s.generateVerificationCode() + sErr := s.repositoryOtp.SaveCodeWithPhoneNumber(ctx, phoneNumber, newCode, s.config.OtpExpireTime) + if sErr != nil { + return richerror.New(op).WithErr(sErr).WithKind(richerror.KindUnexpected) + } + + go s.smsContract.Send(phoneNumber, newCode) + + return nil +} + +func (s Service) LoginOrRegisterDriver(ctx context.Context, phoneNumber string, verifyCode string) (entity.Driver, error) { + const op = "accountService.LoginOrRegisterDriver" + + code, gErr := s.repositoryOtp.GetCodeByPhoneNumber(ctx, phoneNumber) + if gErr != nil { + return entity.Driver{}, richerror.New(op).WithErr(gErr).WithKind(richerror.KindUnexpected) + } + + if code == "" || code != verifyCode { + return entity.Driver{}, richerror.New(op).WithMessage(errmsg.ErrorMsgOtpCodeIsNotValid).WithKind(richerror.KindForbidden) + } + + _, dErr := s.repositoryOtp.DeleteCodeByPhoneNumber(ctx, phoneNumber) + if dErr != nil { + return entity.Driver{}, richerror.New(op).WithErr(dErr).WithKind(richerror.KindUnexpected) + } + + isExist, driver, eErr := s.repository.IsExistDriverByPhoneNumber(ctx, phoneNumber) + if eErr != nil { + return entity.Driver{}, richerror.New(op).WithErr(eErr).WithKind(richerror.KindUnexpected) + } + + if !isExist { + newDriver, cErr := s.repository.CreateDriver(ctx, entity.Driver{ + PhoneNumber: phoneNumber, + }) + if cErr != nil { + return entity.Driver{}, richerror.New(op).WithErr(cErr).WithKind(richerror.KindUnexpected) + } + + driver = newDriver + } + + return driver, 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/app.go b/driverapp/app.go new file mode 100644 index 00000000..b75f90a8 --- /dev/null +++ b/driverapp/app.go @@ -0,0 +1,38 @@ +package driverapp + +import ( + "git.gocasts.ir/ebhomengo/niki/adapter/account" + "git.gocasts.ir/ebhomengo/niki/driverapp/delivery/http" + "git.gocasts.ir/ebhomengo/niki/driverapp/service" + "git.gocasts.ir/ebhomengo/niki/pkg/http_server" + "google.golang.org/grpc" +) + +type Application struct { + svc service.Service + accountClient account.Client + handler http.Handler + httpServer http.Server + config Config +} + +func Setup(config Config, conn *grpc.ClientConn) Application { + driverValidator := service.NewValidator() + accountClient := account.New(conn) + driverSvc := service.NewService(config.DriverSvc, accountClient, driverValidator) + driverHandler := http.NewHandler(driverSvc) + + httpServer := httpserver.New(config.HttpServer) + + return Application{ + svc: driverSvc, + handler: driverHandler, + httpServer: http.New(httpServer, driverHandler), + config: config, + } + +} + +func (app Application) Start() { + app.httpServer.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/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..833b69bd --- /dev/null +++ b/driverapp/service/param.go @@ -0,0 +1,25 @@ +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 { + ID types.ID `json:"id"` + PhoneNumber string `json:"phone_number"` +} + +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..9f40fca4 --- /dev/null +++ b/driverapp/service/service.go @@ -0,0 +1,76 @@ +package service + +import ( + "context" + "fmt" + "time" + + 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 AccountClient interface { + SendOTP(ctx context.Context, phoneNumber string) error + LoginOrRegister(ctx context.Context, req LoginOrRegisterRequest) (LoginOrRegisterResponse, error) +} + +type AuthClient interface { + CreateAccessToken() + CreateRefreshToken() +} + +type Service struct { + config Config + accountClient AccountClient + validator Validator +} + +func NewService(cfg Config, + accountClient AccountClient, + validator Validator) Service { + return Service{ + config: cfg, + accountClient: accountClient, + 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()) + } + + sErr := s.accountClient.SendOTP(ctx, req.PhoneNumber) + if sErr != nil { + return SendOtpResponse{}, richerror.New(op).WithErr(sErr).WithMessage(sErr.Error()) + } + + 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()) + } + + resp, lErr := s.accountClient.LoginOrRegister(ctx, req) + if lErr != nil { + return LoginOrRegisterResponse{}, err + } + + fmt.Println("res:", resp) + + // TODO : CreateAccessToken and create CreateRefreshToken + + return LoginOrRegisterResponse{}, nil +} 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/go.mod b/go.mod index 01ddc255..f391ed78 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -94,7 +95,7 @@ require ( golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.42.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/main.go b/main.go index dba18f22..1111508d 100644 --- a/main.go +++ b/main.go @@ -48,7 +48,7 @@ func MariaDB(cfg config.Config) *mysql.DB { 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/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