diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eee3c58..8b38265 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,29 +2,25 @@ name: Run tests on: [push, pull_request] jobs: - test: name: Test repo runs-on: ubuntu-latest steps: + - name: Set up Go 1.13 + uses: actions/setup-go@v1 + with: + go-version: 1.13 + id: go - - name: Set up Go 1.13 - uses: actions/setup-go@v1 - with: - go-version: 1.13 - id: go - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Get dependencies - run: | - go get -v -t -d ./... - - - name: Run tests - id: tests - env: - IN_TRAVIS_CI: yes - run: go test -v ./... + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + - name: Get dependencies + run: | + go get -v -t -d ./... + - name: Run tests + id: tests + env: + IN_TRAVIS_CI: yes + run: go test -v -p 1 ./... diff --git a/codes/.gitignore b/codes/.gitignore new file mode 100644 index 0000000..086c401 --- /dev/null +++ b/codes/.gitignore @@ -0,0 +1,2 @@ + +codes diff --git a/codes/Makefile b/codes/Makefile new file mode 100644 index 0000000..c2e46d3 --- /dev/null +++ b/codes/Makefile @@ -0,0 +1,22 @@ + +GOPATH:=$(shell go env GOPATH) +.PHONY: init +init: + go get -u github.com/golang/protobuf/proto + go get -u github.com/golang/protobuf/protoc-gen-go + go get github.com/micro/micro/v3/cmd/protoc-gen-micro +.PHONY: proto +proto: + protoc --proto_path=. --micro_out=. --go_out=:. proto/codes.proto + +.PHONY: build +build: + go build -o codes *.go + +.PHONY: test +test: + go test -v ./... -cover + +.PHONY: docker +docker: + docker build . -t codes:latest diff --git a/codes/README.md b/codes/README.md new file mode 100644 index 0000000..1384a9e --- /dev/null +++ b/codes/README.md @@ -0,0 +1,3 @@ +# Codes Service + +The codes service generates codes for use with email / sms verification diff --git a/codes/handler/create.go b/codes/handler/create.go new file mode 100644 index 0000000..13a06f8 --- /dev/null +++ b/codes/handler/create.go @@ -0,0 +1,42 @@ +package handler + +import ( + "context" + "math/rand" + "strconv" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/codes/proto" +) + +func (c *Codes) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error { + // validate the request + if len(req.Identity) == 0 { + return ErrMissingIdentity + } + + // construct the code + code := Code{Code: generateCode(), Identity: req.Identity} + if req.ExpiresAt != nil { + code.ExpiresAt = req.ExpiresAt.AsTime() + } else { + code.ExpiresAt = c.Time().Add(DefaultTTL) + } + + // write to the database + if err := c.DB.Create(&code).Error; err != nil { + logger.Errorf("Error creating code in database: %v", err) + return errors.InternalServerError("DATABASE_ERORR", "Error connecting to database") + } + + // return the code + rsp.Code = code.Code + return nil +} + +// generateCode generates a random 8 digit code +func generateCode() string { + v := rand.Intn(89999999) + 10000000 + return strconv.Itoa(v) +} diff --git a/codes/handler/create_test.go b/codes/handler/create_test.go new file mode 100644 index 0000000..736c6d7 --- /dev/null +++ b/codes/handler/create_test.go @@ -0,0 +1,39 @@ +package handler_test + +import ( + "context" + "testing" + + "github.com/micro/services/codes/handler" + pb "github.com/micro/services/codes/proto" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestCreate(t *testing.T) { + h := testHandler(t) + + t.Run("MissingIdentity", func(t *testing.T) { + var rsp pb.CreateResponse + err := h.Create(context.TODO(), &pb.CreateRequest{}, &rsp) + assert.Equal(t, handler.ErrMissingIdentity, err) + assert.Empty(t, rsp.Code) + }) + + t.Run("NoExpiry", func(t *testing.T) { + var rsp pb.CreateResponse + err := h.Create(context.TODO(), &pb.CreateRequest{Identity: "07503196715"}, &rsp) + assert.NoError(t, err) + assert.NotEmpty(t, rsp.Code) + }) + + t.Run("WithExpiry", func(t *testing.T) { + var rsp pb.CreateResponse + err := h.Create(context.TODO(), &pb.CreateRequest{ + Identity: "demo@m3o.com", + ExpiresAt: timestamppb.Now(), + }, &rsp) + assert.NoError(t, err) + assert.NotEmpty(t, rsp.Code) + }) +} diff --git a/codes/handler/handler.go b/codes/handler/handler.go new file mode 100644 index 0000000..2405778 --- /dev/null +++ b/codes/handler/handler.go @@ -0,0 +1,28 @@ +package handler + +import ( + "time" + + "github.com/micro/micro/v3/service/errors" + "gorm.io/gorm" +) + +var ( + ErrMissingCode = errors.BadRequest("MISSING_CODE", "Missing code") + ErrMissingIdentity = errors.BadRequest("MISSING_IDENTITY", "Missing identity") + ErrInvalidCode = errors.BadRequest("INVALID_CODE", "Invalid code") + ErrExpiredCode = errors.BadRequest("EXPIRED_CODE", "Expired code") + + DefaultTTL = time.Minute * 5 +) + +type Codes struct { + DB *gorm.DB + Time func() time.Time +} + +type Code struct { + Code string `gorm:"index:codeIdentity"` + Identity string `gorm:"index:codeIdentity"` + ExpiresAt time.Time +} diff --git a/codes/handler/handler_test.go b/codes/handler/handler_test.go new file mode 100644 index 0000000..9c38d4d --- /dev/null +++ b/codes/handler/handler_test.go @@ -0,0 +1,30 @@ +package handler_test + +import ( + "testing" + "time" + + "github.com/micro/services/codes/handler" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +func testHandler(t *testing.T) *handler.Codes { + // connect to the database + db, err := gorm.Open(postgres.Open("postgresql://postgres@localhost:5432/codes?sslmode=disable"), &gorm.Config{}) + if err != nil { + t.Fatalf("Error connecting to database: %v", err) + } + + // migrate the database + if err := db.AutoMigrate(&handler.Code{}); err != nil { + t.Fatalf("Error migrating database: %v", err) + } + + // clean any data from a previous run + if err := db.Exec("TRUNCATE TABLE codes CASCADE").Error; err != nil { + t.Fatalf("Error cleaning database: %v", err) + } + + return &handler.Codes{DB: db, Time: time.Now} +} diff --git a/codes/handler/verify.go b/codes/handler/verify.go new file mode 100644 index 0000000..f0257b2 --- /dev/null +++ b/codes/handler/verify.go @@ -0,0 +1,36 @@ +package handler + +import ( + "context" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/codes/proto" + "gorm.io/gorm" +) + +func (c *Codes) Verify(ctx context.Context, req *pb.VerifyRequest, rsp *pb.VerifyResponse) error { + // validate the request + if len(req.Code) == 0 { + return ErrMissingCode + } + if len(req.Identity) == 0 { + return ErrMissingIdentity + } + + // lookup the code + var code Code + if err := c.DB.Where(&Code{Code: req.Code, Identity: req.Identity}).First(&code).Error; err == gorm.ErrRecordNotFound { + return ErrInvalidCode + } else if err != nil { + logger.Errorf("Error reading code from database: %v", err) + return errors.InternalServerError("DATABASE_ERORR", "Error connecting to database") + } + + // check the invite hasn't expired + if code.ExpiresAt.Before(c.Time()) { + return ErrExpiredCode + } + + return nil +} diff --git a/codes/handler/verify_test.go b/codes/handler/verify_test.go new file mode 100644 index 0000000..fd26463 --- /dev/null +++ b/codes/handler/verify_test.go @@ -0,0 +1,60 @@ +package handler_test + +import ( + "context" + "testing" + "time" + + "github.com/micro/services/codes/handler" + pb "github.com/micro/services/codes/proto" + "github.com/stretchr/testify/assert" +) + +func TestVerify(t *testing.T) { + h := testHandler(t) + + t.Run("MissingIdentity", func(t *testing.T) { + var rsp pb.VerifyResponse + err := h.Verify(context.TODO(), &pb.VerifyRequest{Code: "123456"}, &rsp) + assert.Equal(t, handler.ErrMissingIdentity, err) + }) + + t.Run("MissingCode", func(t *testing.T) { + var rsp pb.VerifyResponse + err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "demo@m3o.com"}, &rsp) + assert.Equal(t, handler.ErrMissingCode, err) + }) + + // generate a code to test + var cRsp pb.CreateResponse + err := h.Create(context.TODO(), &pb.CreateRequest{Identity: "demo@m3o.com"}, &cRsp) + assert.NoError(t, err) + + t.Run("IncorrectCode", func(t *testing.T) { + var rsp pb.VerifyResponse + err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "demo@m3o.com", Code: "12345"}, &rsp) + assert.Equal(t, handler.ErrInvalidCode, err) + }) + + t.Run("IncorrectEmail", func(t *testing.T) { + var rsp pb.VerifyResponse + err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "john@m3o.com", Code: cRsp.Code}, &rsp) + assert.Equal(t, handler.ErrInvalidCode, err) + }) + + t.Run("ExpiredCode", func(t *testing.T) { + ot := h.Time + h.Time = func() time.Time { return time.Now().Add(handler.DefaultTTL * 2) } + defer func() { h.Time = ot }() + + var rsp pb.VerifyResponse + err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "demo@m3o.com", Code: cRsp.Code}, &rsp) + assert.Equal(t, handler.ErrExpiredCode, err) + }) + + t.Run("ValidCode", func(t *testing.T) { + var rsp pb.VerifyResponse + err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "demo@m3o.com", Code: cRsp.Code}, &rsp) + assert.NoError(t, err) + }) +} diff --git a/codes/main.go b/codes/main.go new file mode 100644 index 0000000..f9c57b4 --- /dev/null +++ b/codes/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "time" + + "github.com/micro/services/codes/handler" + pb "github.com/micro/services/codes/proto" + + "github.com/micro/micro/v3/service" + "github.com/micro/micro/v3/service/config" + "github.com/micro/micro/v3/service/logger" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +var dbAddress = "postgresql://postgres:postgres@localhost:5432/codes?sslmode=disable" + +func main() { + // Create service + srv := service.New( + service.Name("codes"), + service.Version("latest"), + ) + + // Connect to the database + cfg, err := config.Get("codes.database") + if err != nil { + logger.Fatalf("Error loading config: %v", err) + } + addr := cfg.String(dbAddress) + db, err := gorm.Open(postgres.Open(addr), &gorm.Config{}) + if err != nil { + logger.Fatalf("Error connecting to database: %v", err) + } + if err := db.AutoMigrate(&handler.Code{}); err != nil { + logger.Fatalf("Error migrating database: %v", err) + } + + // Register handler + pb.RegisterCodesHandler(srv.Server(), &handler.Codes{DB: db, Time: time.Now}) + + // Run service + if err := srv.Run(); err != nil { + logger.Fatal(err) + } +} diff --git a/codes/micro.mu b/codes/micro.mu new file mode 100644 index 0000000..653d211 --- /dev/null +++ b/codes/micro.mu @@ -0,0 +1 @@ +service codes diff --git a/codes/proto/codes.pb.go b/codes/proto/codes.pb.go new file mode 100644 index 0000000..3acc5f9 --- /dev/null +++ b/codes/proto/codes.pb.go @@ -0,0 +1,362 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.23.0 +// protoc v3.13.0 +// source: proto/codes.proto + +package codes + +import ( + proto "github.com/golang/protobuf/proto" + timestamp "github.com/golang/protobuf/ptypes/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +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) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type CreateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Identity string `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` // e.g. phone number or email being verified + ExpiresAt *timestamp.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // expiry time for the code, default 5 minutes +} + +func (x *CreateRequest) Reset() { + *x = CreateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_codes_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRequest) ProtoMessage() {} + +func (x *CreateRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_codes_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRequest.ProtoReflect.Descriptor instead. +func (*CreateRequest) Descriptor() ([]byte, []int) { + return file_proto_codes_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateRequest) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *CreateRequest) GetExpiresAt() *timestamp.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +type CreateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` +} + +func (x *CreateResponse) Reset() { + *x = CreateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_codes_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateResponse) ProtoMessage() {} + +func (x *CreateResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_codes_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateResponse.ProtoReflect.Descriptor instead. +func (*CreateResponse) Descriptor() ([]byte, []int) { + return file_proto_codes_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateResponse) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +type VerifyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` + Identity string `protobuf:"bytes,2,opt,name=identity,proto3" json:"identity,omitempty"` +} + +func (x *VerifyRequest) Reset() { + *x = VerifyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_codes_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VerifyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyRequest) ProtoMessage() {} + +func (x *VerifyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_codes_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyRequest.ProtoReflect.Descriptor instead. +func (*VerifyRequest) Descriptor() ([]byte, []int) { + return file_proto_codes_proto_rawDescGZIP(), []int{2} +} + +func (x *VerifyRequest) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *VerifyRequest) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +type VerifyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *VerifyResponse) Reset() { + *x = VerifyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_codes_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VerifyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyResponse) ProtoMessage() {} + +func (x *VerifyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_codes_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyResponse.ProtoReflect.Descriptor instead. +func (*VerifyResponse) Descriptor() ([]byte, []int) { + return file_proto_codes_proto_rawDescGZIP(), []int{3} +} + +var File_proto_codes_proto protoreflect.FileDescriptor + +var file_proto_codes_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x66, 0x0a, 0x0d, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x73, 0x41, 0x74, 0x22, 0x24, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x3f, 0x0a, 0x0d, 0x56, 0x65, 0x72, + 0x69, 0x66, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x10, 0x0a, 0x0e, 0x56, 0x65, + 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x75, 0x0a, 0x05, + 0x43, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, + 0x14, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x06, + 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x12, 0x14, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x56, + 0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x0d, 0x5a, 0x0b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x63, 0x6f, 0x64, + 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proto_codes_proto_rawDescOnce sync.Once + file_proto_codes_proto_rawDescData = file_proto_codes_proto_rawDesc +) + +func file_proto_codes_proto_rawDescGZIP() []byte { + file_proto_codes_proto_rawDescOnce.Do(func() { + file_proto_codes_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_codes_proto_rawDescData) + }) + return file_proto_codes_proto_rawDescData +} + +var file_proto_codes_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proto_codes_proto_goTypes = []interface{}{ + (*CreateRequest)(nil), // 0: codes.CreateRequest + (*CreateResponse)(nil), // 1: codes.CreateResponse + (*VerifyRequest)(nil), // 2: codes.VerifyRequest + (*VerifyResponse)(nil), // 3: codes.VerifyResponse + (*timestamp.Timestamp)(nil), // 4: google.protobuf.Timestamp +} +var file_proto_codes_proto_depIdxs = []int32{ + 4, // 0: codes.CreateRequest.expires_at:type_name -> google.protobuf.Timestamp + 0, // 1: codes.Codes.Create:input_type -> codes.CreateRequest + 2, // 2: codes.Codes.Verify:input_type -> codes.VerifyRequest + 1, // 3: codes.Codes.Create:output_type -> codes.CreateResponse + 3, // 4: codes.Codes.Verify:output_type -> codes.VerifyResponse + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proto_codes_proto_init() } +func file_proto_codes_proto_init() { + if File_proto_codes_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_codes_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_codes_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_codes_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VerifyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_codes_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VerifyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_codes_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_codes_proto_goTypes, + DependencyIndexes: file_proto_codes_proto_depIdxs, + MessageInfos: file_proto_codes_proto_msgTypes, + }.Build() + File_proto_codes_proto = out.File + file_proto_codes_proto_rawDesc = nil + file_proto_codes_proto_goTypes = nil + file_proto_codes_proto_depIdxs = nil +} diff --git a/codes/proto/codes.pb.micro.go b/codes/proto/codes.pb.micro.go new file mode 100644 index 0000000..eee3342 --- /dev/null +++ b/codes/proto/codes.pb.micro.go @@ -0,0 +1,111 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: proto/codes.proto + +package codes + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + _ "github.com/golang/protobuf/ptypes/timestamp" + math "math" +) + +import ( + context "context" + api "github.com/micro/micro/v3/service/api" + client "github.com/micro/micro/v3/service/client" + server "github.com/micro/micro/v3/service/server" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +// Reference imports to suppress errors if they are not otherwise used. +var _ api.Endpoint +var _ context.Context +var _ client.Option +var _ server.Option + +// Api Endpoints for Codes service + +func NewCodesEndpoints() []*api.Endpoint { + return []*api.Endpoint{} +} + +// Client API for Codes service + +type CodesService interface { + Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error) + Verify(ctx context.Context, in *VerifyRequest, opts ...client.CallOption) (*VerifyResponse, error) +} + +type codesService struct { + c client.Client + name string +} + +func NewCodesService(name string, c client.Client) CodesService { + return &codesService{ + c: c, + name: name, + } +} + +func (c *codesService) Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error) { + req := c.c.NewRequest(c.name, "Codes.Create", in) + out := new(CreateResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *codesService) Verify(ctx context.Context, in *VerifyRequest, opts ...client.CallOption) (*VerifyResponse, error) { + req := c.c.NewRequest(c.name, "Codes.Verify", in) + out := new(VerifyResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Codes service + +type CodesHandler interface { + Create(context.Context, *CreateRequest, *CreateResponse) error + Verify(context.Context, *VerifyRequest, *VerifyResponse) error +} + +func RegisterCodesHandler(s server.Server, hdlr CodesHandler, opts ...server.HandlerOption) error { + type codes interface { + Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error + Verify(ctx context.Context, in *VerifyRequest, out *VerifyResponse) error + } + type Codes struct { + codes + } + h := &codesHandler{hdlr} + return s.Handle(s.NewHandler(&Codes{h}, opts...)) +} + +type codesHandler struct { + CodesHandler +} + +func (h *codesHandler) Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error { + return h.CodesHandler.Create(ctx, in, out) +} + +func (h *codesHandler) Verify(ctx context.Context, in *VerifyRequest, out *VerifyResponse) error { + return h.CodesHandler.Verify(ctx, in, out) +} diff --git a/codes/proto/codes.proto b/codes/proto/codes.proto new file mode 100644 index 0000000..03c26a1 --- /dev/null +++ b/codes/proto/codes.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package codes; +option go_package = "proto;codes"; +import "google/protobuf/timestamp.proto"; + +service Codes { + rpc Create(CreateRequest) returns (CreateResponse); + rpc Verify(VerifyRequest) returns (VerifyResponse); +} + +message CreateRequest { + string identity = 1; // e.g. phone number or email being verified + google.protobuf.Timestamp expires_at = 2; // expiry time for the code, default 5 minutes +} + +message CreateResponse { + string code = 1; +} + +message VerifyRequest { + string code = 1; + string identity = 2; +} + +message VerifyResponse {} \ No newline at end of file diff --git a/users/Makefile b/users/Makefile index 0761276..3149dce 100644 --- a/users/Makefile +++ b/users/Makefile @@ -20,7 +20,7 @@ build: .PHONY: test test: - go test -v ./... -cover + go test -v -p 1 ./... .PHONY: docker docker: diff --git a/users/handler/create.go b/users/handler/create.go new file mode 100644 index 0000000..f89a849 --- /dev/null +++ b/users/handler/create.go @@ -0,0 +1,74 @@ +package handler + +import ( + "context" + "strings" + "time" + + "github.com/google/uuid" + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/users/proto" + "gorm.io/gorm" +) + +// Create a user +func (u *Users) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error { + // validate the request + if len(req.FirstName) == 0 { + return ErrMissingFirstName + } + if len(req.LastName) == 0 { + return ErrMissingLastName + } + if len(req.Email) == 0 { + return ErrMissingEmail + } + if !isEmailValid(req.Email) { + return ErrInvalidEmail + } + if len(req.Password) < 8 { + return ErrInvalidPassword + } + + // hash and salt the password using bcrypt + phash, err := hashAndSalt(req.Password) + if err != nil { + logger.Errorf("Error hasing and salting password: %v", err) + return errors.InternalServerError("HASHING_ERROR", "Error hashing password") + } + + return u.DB.Transaction(func(tx *gorm.DB) error { + // write the user to the database + user := &User{ + ID: uuid.New().String(), + FirstName: req.FirstName, + LastName: req.LastName, + Email: strings.ToLower(req.Email), + Password: phash, + } + err = u.DB.Create(user).Error + if err != nil && strings.Contains(err.Error(), "idx_users_email") { + return ErrDuplicateEmail + } else if err != nil { + logger.Errorf("Error writing to the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // generate a token for the user + token := Token{ + UserID: user.ID, + Key: uuid.New().String(), + ExpiresAt: u.Time().Add(time.Hour * 24 * 7), + } + if err := tx.Create(&token).Error; err != nil { + logger.Errorf("Error writing to the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // serialize the response + rsp.User = user.Serialize() + rsp.Token = token.Key + return nil + }) +} diff --git a/users/handler/create_test.go b/users/handler/create_test.go new file mode 100644 index 0000000..ef780e3 --- /dev/null +++ b/users/handler/create_test.go @@ -0,0 +1,129 @@ +package handler_test + +import ( + "context" + "testing" + + "github.com/micro/services/users/handler" + pb "github.com/micro/services/users/proto" + "github.com/stretchr/testify/assert" +) + +func TestCreate(t *testing.T) { + tt := []struct { + Name string + FirstName string + LastName string + Email string + Password string + Error error + }{ + { + Name: "MissingFirstName", + LastName: "Doe", + Email: "john@doe.com", + Password: "password", + Error: handler.ErrMissingFirstName, + }, + { + Name: "MissingLastName", + FirstName: "John", + Email: "john@doe.com", + Password: "password", + Error: handler.ErrMissingLastName, + }, + { + Name: "MissingEmail", + FirstName: "John", + LastName: "Doe", + Password: "password", + Error: handler.ErrMissingEmail, + }, + { + Name: "InvalidEmail", + FirstName: "John", + LastName: "Doe", + Password: "password", + Email: "foo.foo.foo", + Error: handler.ErrInvalidEmail, + }, + { + Name: "InvalidPassword", + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "pwd", + Error: handler.ErrInvalidPassword, + }, + } + + // test the validations + h := testHandler(t) + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + err := h.Create(context.TODO(), &pb.CreateRequest{ + FirstName: tc.FirstName, + LastName: tc.LastName, + Email: tc.Email, + Password: tc.Password, + }, &pb.CreateResponse{}) + assert.Equal(t, tc.Error, err) + }) + } + + t.Run("Valid", func(t *testing.T) { + var rsp pb.CreateResponse + req := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &req, &rsp) + + assert.NoError(t, err) + u := rsp.User + if u == nil { + t.Fatalf("No user returned") + } + assert.NotEmpty(t, u.Id) + assert.Equal(t, req.FirstName, u.FirstName) + assert.Equal(t, req.LastName, u.LastName) + assert.Equal(t, req.Email, u.Email) + assert.NotEmpty(t, rsp.Token) + }) + + t.Run("DuplicateEmail", func(t *testing.T) { + var rsp pb.CreateResponse + req := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &req, &rsp) + assert.Equal(t, handler.ErrDuplicateEmail, err) + assert.Nil(t, rsp.User) + }) + + t.Run("DifferentEmail", func(t *testing.T) { + var rsp pb.CreateResponse + req := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "johndoe@gmail.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &req, &rsp) + + assert.NoError(t, err) + u := rsp.User + if u == nil { + t.Fatalf("No user returned") + } + assert.NotEmpty(t, u.Id) + assert.Equal(t, req.FirstName, u.FirstName) + assert.Equal(t, req.LastName, u.LastName) + assert.Equal(t, req.Email, u.Email) + }) +} diff --git a/users/handler/delete.go b/users/handler/delete.go new file mode 100644 index 0000000..0b0d7de --- /dev/null +++ b/users/handler/delete.go @@ -0,0 +1,34 @@ +package handler + +import ( + "context" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/users/proto" + "gorm.io/gorm" +) + +// Delete a user +func (u *Users) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error { + // validate the request + if len(req.Id) == 0 { + return ErrMissingID + } + + // delete the users tokens + return u.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Delete(&Token{}, &Token{UserID: req.Id}).Error; err != nil { + logger.Errorf("Error writing to the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // delete from the database + if err := tx.Delete(&User{}, &User{ID: req.Id}).Error; err != nil { + logger.Errorf("Error writing to the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + return nil + }) +} diff --git a/users/handler/delete_test.go b/users/handler/delete_test.go new file mode 100644 index 0000000..36710b0 --- /dev/null +++ b/users/handler/delete_test.go @@ -0,0 +1,56 @@ +package handler_test + +import ( + "context" + "testing" + + "github.com/micro/services/users/handler" + pb "github.com/micro/services/users/proto" + "github.com/stretchr/testify/assert" +) + +func TestDelete(t *testing.T) { + h := testHandler(t) + + t.Run("MissingID", func(t *testing.T) { + err := h.Delete(context.TODO(), &pb.DeleteRequest{}, &pb.DeleteResponse{}) + assert.Equal(t, handler.ErrMissingID, err) + }) + + // create some mock data + var cRsp pb.CreateResponse + cReq := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &cReq, &cRsp) + assert.NoError(t, err) + if cRsp.User == nil { + t.Fatal("No user returned") + return + } + + t.Run("Valid", func(t *testing.T) { + err := h.Delete(context.TODO(), &pb.DeleteRequest{ + Id: cRsp.User.Id, + }, &pb.DeleteResponse{}) + assert.NoError(t, err) + + // check it was actually deleted + var rsp pb.ReadResponse + err = h.Read(context.TODO(), &pb.ReadRequest{ + Ids: []string{cRsp.User.Id}, + }, &rsp) + assert.NoError(t, err) + assert.Nil(t, rsp.Users[cRsp.User.Id]) + }) + + t.Run("Retry", func(t *testing.T) { + err := h.Delete(context.TODO(), &pb.DeleteRequest{ + Id: cRsp.User.Id, + }, &pb.DeleteResponse{}) + assert.NoError(t, err) + }) +} diff --git a/users/handler/handler.go b/users/handler/handler.go index b3f2732..d78a9c8 100644 --- a/users/handler/handler.go +++ b/users/handler/handler.go @@ -1,14 +1,10 @@ package handler import ( - "context" "regexp" - "strings" "time" - "github.com/google/uuid" "github.com/micro/micro/v3/service/errors" - "github.com/micro/micro/v3/service/logger" pb "github.com/micro/services/users/proto" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" @@ -21,6 +17,7 @@ var ( ErrDuplicateEmail = errors.BadRequest("DUPLICATE_EMAIL", "A user with this email address already exists") ErrInvalidEmail = errors.BadRequest("INVALID_EMAIL", "The email provided is invalid") ErrInvalidPassword = errors.BadRequest("INVALID_PASSWORD", "Password must be at least 8 characters long") + ErrMissingEmails = errors.BadRequest("MISSING_EMAILS", "One or more emails are required") ErrMissingIDs = errors.BadRequest("MISSING_IDS", "One or more ids are required") ErrMissingID = errors.BadRequest("MISSING_ID", "Missing ID") ErrMissingToken = errors.BadRequest("MISSING_TOKEN", "Missing token") @@ -65,288 +62,6 @@ type Users struct { Time func() time.Time } -// Create a user -func (u *Users) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error { - // validate the request - if len(req.FirstName) == 0 { - return ErrMissingFirstName - } - if len(req.LastName) == 0 { - return ErrMissingLastName - } - if len(req.Email) == 0 { - return ErrMissingEmail - } - if !isEmailValid(req.Email) { - return ErrInvalidEmail - } - if len(req.Password) < 8 { - return ErrInvalidPassword - } - - // hash and salt the password using bcrypt - phash, err := hashAndSalt(req.Password) - if err != nil { - logger.Errorf("Error hasing and salting password: %v", err) - return errors.InternalServerError("HASHING_ERROR", "Error hashing password") - } - - return u.DB.Transaction(func(tx *gorm.DB) error { - // write the user to the database - user := &User{ - ID: uuid.New().String(), - FirstName: req.FirstName, - LastName: req.LastName, - Email: strings.ToLower(req.Email), - Password: phash, - } - err = u.DB.Create(user).Error - if err != nil && strings.Contains(err.Error(), "idx_users_email") { - return ErrDuplicateEmail - } else if err != nil { - logger.Errorf("Error writing to the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // generate a token for the user - token := Token{ - UserID: user.ID, - Key: uuid.New().String(), - ExpiresAt: u.Time().Add(time.Hour * 24 * 7), - } - if err := tx.Create(&token).Error; err != nil { - logger.Errorf("Error writing to the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // serialize the response - rsp.User = user.Serialize() - rsp.Token = token.Key - return nil - }) -} - -// Read users using ID -func (u *Users) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error { - // validate the request - if len(req.Ids) == 0 { - return ErrMissingIDs - } - - // query the database - var users []User - if err := u.DB.Model(&User{}).Where("id IN (?)", req.Ids).Find(&users).Error; err != nil { - logger.Errorf("Error reading from the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // serialize the response - rsp.Users = make(map[string]*pb.User, len(users)) - for _, u := range users { - rsp.Users[u.ID] = u.Serialize() - } - return nil -} - -// Update a user -func (u *Users) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error { - // validate the request - if len(req.Id) == 0 { - return ErrMissingID - } - if req.FirstName != nil && len(req.FirstName.Value) == 0 { - return ErrMissingFirstName - } - if req.LastName != nil && len(req.LastName.Value) == 0 { - return ErrMissingLastName - } - if req.Email != nil && len(req.Email.Value) == 0 { - return ErrMissingEmail - } - if req.Email != nil && !isEmailValid(req.Email.Value) { - return ErrInvalidEmail - } - - // lookup the user - var user User - if err := u.DB.Where(&User{ID: req.Id}).First(&user).Error; err == gorm.ErrRecordNotFound { - return ErrNotFound - } else if err != nil { - logger.Errorf("Error reading from the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // assign the updated values - if req.FirstName != nil { - user.FirstName = req.FirstName.Value - } - if req.LastName != nil { - user.LastName = req.LastName.Value - } - if req.Email != nil { - user.Email = strings.ToLower(req.Email.Value) - } - - // write the user to the database - err := u.DB.Save(user).Error - if err != nil && strings.Contains(err.Error(), "idx_users_email") { - return ErrDuplicateEmail - } else if err != nil { - logger.Errorf("Error writing to the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // serialize the user - rsp.User = user.Serialize() - return nil -} - -// Delete a user -func (u *Users) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error { - // validate the request - if len(req.Id) == 0 { - return ErrMissingID - } - - // delete the users tokens - return u.DB.Transaction(func(tx *gorm.DB) error { - if err := tx.Delete(&Token{}, &Token{UserID: req.Id}).Error; err != nil { - logger.Errorf("Error writing to the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // delete from the database - if err := tx.Delete(&User{}, &User{ID: req.Id}).Error; err != nil { - logger.Errorf("Error writing to the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - return nil - }) -} - -// List all users -func (u *Users) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error { - // query the database - var users []User - if err := u.DB.Model(&User{}).Find(&users).Error; err != nil { - logger.Errorf("Error reading from the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // serialize the response - rsp.Users = make([]*pb.User, len(users)) - for i, u := range users { - rsp.Users[i] = u.Serialize() - } - return nil -} - -// Login using email and password returns the users profile and a token -func (u *Users) Login(ctx context.Context, req *pb.LoginRequest, rsp *pb.LoginResponse) error { - // validate the request - if len(req.Email) == 0 { - return ErrMissingEmail - } - if len(req.Password) == 0 { - return ErrInvalidPassword - } - - return u.DB.Transaction(func(tx *gorm.DB) error { - // lookup the user - var user User - if err := tx.Where(&User{Email: req.Email}).First(&user).Error; err == gorm.ErrRecordNotFound { - return ErrNotFound - } else if err != nil { - logger.Errorf("Error reading from the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // compare the passwords - if !passwordsMatch(user.Password, req.Password) { - return ErrIncorrectPassword - } - - // generate a token for the user - token := Token{ - UserID: user.ID, - Key: uuid.New().String(), - ExpiresAt: u.Time().Add(tokenTTL), - } - if err := tx.Create(&token).Error; err != nil { - logger.Errorf("Error writing to the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // serialize the response - rsp.Token = token.Key - rsp.User = user.Serialize() - return nil - }) -} - -// Logout expires all tokens for the user -func (u *Users) Logout(ctx context.Context, req *pb.LogoutRequest, rsp *pb.LogoutResponse) error { - // validate the request - if len(req.Id) == 0 { - return ErrMissingID - } - - return u.DB.Transaction(func(tx *gorm.DB) error { - // lookup the user - var user User - if err := tx.Where(&User{ID: req.Id}).Preload("Tokens").First(&user).Error; err == gorm.ErrRecordNotFound { - return ErrNotFound - } else if err != nil { - logger.Errorf("Error reading from the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // delete the tokens - if err := tx.Delete(user.Tokens).Error; err != nil { - logger.Errorf("Error deleting from the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - return nil - }) -} - -// Validate a token, each time a token is validated it extends its lifetime for another week -func (u *Users) Validate(ctx context.Context, req *pb.ValidateRequest, rsp *pb.ValidateResponse) error { - // validate the request - if len(req.Token) == 0 { - return ErrMissingToken - } - - return u.DB.Transaction(func(tx *gorm.DB) error { - // lookup the token - var token Token - if err := tx.Where(&Token{Key: req.Token}).Preload("User").First(&token).Error; err == gorm.ErrRecordNotFound { - return ErrInvalidToken - } else if err != nil { - logger.Errorf("Error reading from the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // ensure the token is valid - if u.Time().After(token.ExpiresAt) { - return ErrTokenExpired - } - - // extend the token for another lifetime - token.ExpiresAt = u.Time().Add(tokenTTL) - if err := tx.Save(&token).Error; err != nil { - logger.Errorf("Error writing to the database: %v", err) - return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") - } - - // serialize the response - rsp.User = token.User.Serialize() - return nil - }) -} - // isEmailValid checks if the email provided passes the required structure and length. func isEmailValid(e string) bool { if len(e) < 3 && len(e) > 254 { diff --git a/users/handler/handler_test.go b/users/handler/handler_test.go index 3241287..007ad27 100644 --- a/users/handler/handler_test.go +++ b/users/handler/handler_test.go @@ -1,13 +1,10 @@ package handler_test import ( - "context" "testing" "time" - "github.com/google/uuid" "github.com/stretchr/testify/assert" - "google.golang.org/protobuf/types/known/wrapperspb" "github.com/micro/services/users/handler" pb "github.com/micro/services/users/proto" @@ -35,627 +32,6 @@ func testHandler(t *testing.T) *handler.Users { return &handler.Users{DB: db, Time: time.Now} } -func TestCreate(t *testing.T) { - tt := []struct { - Name string - FirstName string - LastName string - Email string - Password string - Error error - }{ - { - Name: "MissingFirstName", - LastName: "Doe", - Email: "john@doe.com", - Password: "password", - Error: handler.ErrMissingFirstName, - }, - { - Name: "MissingLastName", - FirstName: "John", - Email: "john@doe.com", - Password: "password", - Error: handler.ErrMissingLastName, - }, - { - Name: "MissingEmail", - FirstName: "John", - LastName: "Doe", - Password: "password", - Error: handler.ErrMissingEmail, - }, - { - Name: "InvalidEmail", - FirstName: "John", - LastName: "Doe", - Password: "password", - Email: "foo.foo.foo", - Error: handler.ErrInvalidEmail, - }, - { - Name: "InvalidPassword", - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "pwd", - Error: handler.ErrInvalidPassword, - }, - } - - // test the validations - h := testHandler(t) - for _, tc := range tt { - t.Run(tc.Name, func(t *testing.T) { - err := h.Create(context.TODO(), &pb.CreateRequest{ - FirstName: tc.FirstName, - LastName: tc.LastName, - Email: tc.Email, - Password: tc.Password, - }, &pb.CreateResponse{}) - assert.Equal(t, tc.Error, err) - }) - } - - t.Run("Valid", func(t *testing.T) { - var rsp pb.CreateResponse - req := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &req, &rsp) - - assert.NoError(t, err) - u := rsp.User - if u == nil { - t.Fatalf("No user returned") - } - assert.NotEmpty(t, u.Id) - assert.Equal(t, req.FirstName, u.FirstName) - assert.Equal(t, req.LastName, u.LastName) - assert.Equal(t, req.Email, u.Email) - assert.NotEmpty(t, rsp.Token) - }) - - t.Run("DuplicateEmail", func(t *testing.T) { - var rsp pb.CreateResponse - req := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &req, &rsp) - assert.Equal(t, handler.ErrDuplicateEmail, err) - assert.Nil(t, rsp.User) - }) - - t.Run("DifferentEmail", func(t *testing.T) { - var rsp pb.CreateResponse - req := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "johndoe@gmail.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &req, &rsp) - - assert.NoError(t, err) - u := rsp.User - if u == nil { - t.Fatalf("No user returned") - } - assert.NotEmpty(t, u.Id) - assert.Equal(t, req.FirstName, u.FirstName) - assert.Equal(t, req.LastName, u.LastName) - assert.Equal(t, req.Email, u.Email) - }) -} - -func TestRead(t *testing.T) { - h := testHandler(t) - - t.Run("MissingIDs", func(t *testing.T) { - var rsp pb.ReadResponse - err := h.Read(context.TODO(), &pb.ReadRequest{}, &rsp) - assert.Equal(t, handler.ErrMissingIDs, err) - assert.Nil(t, rsp.Users) - }) - - t.Run("NotFound", func(t *testing.T) { - var rsp pb.ReadResponse - err := h.Read(context.TODO(), &pb.ReadRequest{Ids: []string{"foo"}}, &rsp) - assert.Nil(t, err) - if rsp.Users == nil { - t.Fatal("Expected the users object to not be nil") - } - assert.Nil(t, rsp.Users["foo"]) - }) - - // create some mock data - var rsp1 pb.CreateResponse - req1 := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &req1, &rsp1) - assert.NoError(t, err) - if rsp1.User == nil { - t.Fatal("No user returned") - return - } - - var rsp2 pb.CreateResponse - req2 := pb.CreateRequest{ - FirstName: "Apple", - LastName: "Tree", - Email: "apple@tree.com", - Password: "passwordabc", - } - err = h.Create(context.TODO(), &req2, &rsp2) - assert.NoError(t, err) - if rsp2.User == nil { - t.Fatal("No user returned") - return - } - - // test the read - var rsp pb.ReadResponse - err = h.Read(context.TODO(), &pb.ReadRequest{ - Ids: []string{rsp1.User.Id, rsp2.User.Id}, - }, &rsp) - assert.NoError(t, err) - - if rsp.Users == nil { - t.Fatal("Users not returned") - return - } - assert.NotNil(t, rsp.Users[rsp1.User.Id]) - assert.NotNil(t, rsp.Users[rsp2.User.Id]) - - // check the users match - if u := rsp.Users[rsp1.User.Id]; u != nil { - assert.Equal(t, rsp1.User.Id, u.Id) - assert.Equal(t, rsp1.User.FirstName, u.FirstName) - assert.Equal(t, rsp1.User.LastName, u.LastName) - assert.Equal(t, rsp1.User.Email, u.Email) - } - if u := rsp.Users[rsp2.User.Id]; u != nil { - assert.Equal(t, rsp2.User.Id, u.Id) - assert.Equal(t, rsp2.User.FirstName, u.FirstName) - assert.Equal(t, rsp2.User.LastName, u.LastName) - assert.Equal(t, rsp2.User.Email, u.Email) - } -} - -func TestUpdate(t *testing.T) { - h := testHandler(t) - - t.Run("MissingID", func(t *testing.T) { - var rsp pb.UpdateResponse - err := h.Update(context.TODO(), &pb.UpdateRequest{}, &rsp) - assert.Equal(t, handler.ErrMissingID, err) - assert.Nil(t, rsp.User) - }) - - t.Run("NotFound", func(t *testing.T) { - var rsp pb.UpdateResponse - err := h.Update(context.TODO(), &pb.UpdateRequest{Id: "foo"}, &rsp) - assert.Equal(t, handler.ErrNotFound, err) - assert.Nil(t, rsp.User) - }) - - // create some mock data - var cRsp1 pb.CreateResponse - cReq1 := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &cReq1, &cRsp1) - assert.NoError(t, err) - if cRsp1.User == nil { - t.Fatal("No user returned") - return - } - - var cRsp2 pb.CreateResponse - cReq2 := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "johndoe@gmail.com", - Password: "passwordabc", - } - err = h.Create(context.TODO(), &cReq2, &cRsp2) - assert.NoError(t, err) - if cRsp2.User == nil { - t.Fatal("No user returned") - return - } - - t.Run("BlankFirstName", func(t *testing.T) { - var rsp pb.UpdateResponse - err := h.Update(context.TODO(), &pb.UpdateRequest{ - Id: cRsp1.User.Id, FirstName: &wrapperspb.StringValue{}, - }, &rsp) - assert.Equal(t, handler.ErrMissingFirstName, err) - assert.Nil(t, rsp.User) - }) - - t.Run("BlankLastName", func(t *testing.T) { - var rsp pb.UpdateResponse - err := h.Update(context.TODO(), &pb.UpdateRequest{ - Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{}, - }, &rsp) - assert.Equal(t, handler.ErrMissingLastName, err) - assert.Nil(t, rsp.User) - }) - - t.Run("BlankLastName", func(t *testing.T) { - var rsp pb.UpdateResponse - err := h.Update(context.TODO(), &pb.UpdateRequest{ - Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{}, - }, &rsp) - assert.Equal(t, handler.ErrMissingLastName, err) - assert.Nil(t, rsp.User) - }) - - t.Run("BlankEmail", func(t *testing.T) { - var rsp pb.UpdateResponse - err := h.Update(context.TODO(), &pb.UpdateRequest{ - Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{}, - }, &rsp) - assert.Equal(t, handler.ErrMissingEmail, err) - assert.Nil(t, rsp.User) - }) - - t.Run("InvalidEmail", func(t *testing.T) { - var rsp pb.UpdateResponse - err := h.Update(context.TODO(), &pb.UpdateRequest{ - Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: "foo.bar"}, - }, &rsp) - assert.Equal(t, handler.ErrInvalidEmail, err) - assert.Nil(t, rsp.User) - }) - - t.Run("EmailAlreadyExists", func(t *testing.T) { - var rsp pb.UpdateResponse - err := h.Update(context.TODO(), &pb.UpdateRequest{ - Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: cRsp2.User.Email}, - }, &rsp) - assert.Equal(t, handler.ErrDuplicateEmail, err) - assert.Nil(t, rsp.User) - }) - - t.Run("Valid", func(t *testing.T) { - uReq := pb.UpdateRequest{ - Id: cRsp1.User.Id, - Email: &wrapperspb.StringValue{Value: "foobar@gmail.com"}, - FirstName: &wrapperspb.StringValue{Value: "Foo"}, - LastName: &wrapperspb.StringValue{Value: "Bar"}, - } - var uRsp pb.UpdateResponse - err := h.Update(context.TODO(), &uReq, &uRsp) - assert.NoError(t, err) - if uRsp.User == nil { - t.Error("No user returned") - return - } - assert.Equal(t, cRsp1.User.Id, uRsp.User.Id) - assert.Equal(t, uReq.Email.Value, uRsp.User.Email) - assert.Equal(t, uReq.FirstName.Value, uRsp.User.FirstName) - assert.Equal(t, uReq.LastName.Value, uRsp.User.LastName) - }) -} - -func TestDelete(t *testing.T) { - h := testHandler(t) - - t.Run("MissingID", func(t *testing.T) { - err := h.Delete(context.TODO(), &pb.DeleteRequest{}, &pb.DeleteResponse{}) - assert.Equal(t, handler.ErrMissingID, err) - }) - - // create some mock data - var cRsp pb.CreateResponse - cReq := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &cReq, &cRsp) - assert.NoError(t, err) - if cRsp.User == nil { - t.Fatal("No user returned") - return - } - - t.Run("Valid", func(t *testing.T) { - err := h.Delete(context.TODO(), &pb.DeleteRequest{ - Id: cRsp.User.Id, - }, &pb.DeleteResponse{}) - assert.NoError(t, err) - - // check it was actually deleted - var rsp pb.ReadResponse - err = h.Read(context.TODO(), &pb.ReadRequest{ - Ids: []string{cRsp.User.Id}, - }, &rsp) - assert.NoError(t, err) - assert.Nil(t, rsp.Users[cRsp.User.Id]) - }) - - t.Run("Retry", func(t *testing.T) { - err := h.Delete(context.TODO(), &pb.DeleteRequest{ - Id: cRsp.User.Id, - }, &pb.DeleteResponse{}) - assert.NoError(t, err) - }) -} - -func TestList(t *testing.T) { - h := testHandler(t) - - // create some mock data - var cRsp1 pb.CreateResponse - cReq1 := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &cReq1, &cRsp1) - assert.NoError(t, err) - if cRsp1.User == nil { - t.Fatal("No user returned") - return - } - - var cRsp2 pb.CreateResponse - cReq2 := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "johndoe@gmail.com", - Password: "passwordabc", - } - err = h.Create(context.TODO(), &cReq2, &cRsp2) - assert.NoError(t, err) - if cRsp2.User == nil { - t.Fatal("No user returned") - return - } - - var rsp pb.ListResponse - err = h.List(context.TODO(), &pb.ListRequest{}, &rsp) - assert.NoError(t, err) - if rsp.Users == nil { - t.Error("No users returned") - return - } - - var u1Found, u2Found bool - for _, u := range rsp.Users { - switch u.Id { - case cRsp1.User.Id: - assertUsersMatch(t, cRsp1.User, u) - u1Found = true - case cRsp2.User.Id: - assertUsersMatch(t, cRsp2.User, u) - u2Found = true - default: - t.Fatal("Unexpected user returned") - return - } - } - assert.True(t, u1Found) - assert.True(t, u2Found) -} - -func TestLogin(t *testing.T) { - h := testHandler(t) - - // create some mock data - var cRsp pb.CreateResponse - cReq := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &cReq, &cRsp) - assert.NoError(t, err) - if cRsp.User == nil { - t.Fatal("No user returned") - return - } - - tt := []struct { - Name string - Email string - Password string - Error error - User *pb.User - }{ - { - Name: "MissingEmail", - Password: "passwordabc", - Error: handler.ErrMissingEmail, - }, - { - Name: "MissingPassword", - Email: "john@doe.com", - Error: handler.ErrInvalidPassword, - }, - { - Name: "UserNotFound", - Email: "foo@bar.com", - Password: "passwordabc", - Error: handler.ErrNotFound, - }, - { - Name: "IncorrectPassword", - Email: "john@doe.com", - Password: "passwordabcdef", - Error: handler.ErrIncorrectPassword, - }, - { - Name: "Valid", - Email: "john@doe.com", - Password: "passwordabc", - User: cRsp.User, - }, - } - - for _, tc := range tt { - t.Run(tc.Name, func(t *testing.T) { - var rsp pb.LoginResponse - err := h.Login(context.TODO(), &pb.LoginRequest{ - Email: tc.Email, Password: tc.Password, - }, &rsp) - assert.Equal(t, tc.Error, err) - - if tc.User != nil { - assertUsersMatch(t, tc.User, rsp.User) - assert.NotEmpty(t, rsp.Token) - } else { - assert.Nil(t, tc.User) - } - }) - } -} - -func TestLogout(t *testing.T) { - h := testHandler(t) - - t.Run("MissingUserID", func(t *testing.T) { - err := h.Logout(context.TODO(), &pb.LogoutRequest{}, &pb.LogoutResponse{}) - assert.Equal(t, handler.ErrMissingID, err) - }) - - t.Run("UserNotFound", func(t *testing.T) { - err := h.Logout(context.TODO(), &pb.LogoutRequest{Id: uuid.New().String()}, &pb.LogoutResponse{}) - assert.Equal(t, handler.ErrNotFound, err) - }) - - t.Run("Valid", func(t *testing.T) { - // create some mock data - var cRsp pb.CreateResponse - cReq := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &cReq, &cRsp) - assert.NoError(t, err) - if cRsp.User == nil { - t.Fatal("No user returned") - return - } - - err = h.Logout(context.TODO(), &pb.LogoutRequest{Id: cRsp.User.Id}, &pb.LogoutResponse{}) - assert.NoError(t, err) - - err = h.Validate(context.TODO(), &pb.ValidateRequest{Token: cRsp.Token}, &pb.ValidateResponse{}) - assert.Error(t, err) - }) -} - -func TestValidate(t *testing.T) { - h := testHandler(t) - - // create some mock data - var cRsp1 pb.CreateResponse - cReq1 := pb.CreateRequest{ - FirstName: "John", - LastName: "Doe", - Email: "john@doe.com", - Password: "passwordabc", - } - err := h.Create(context.TODO(), &cReq1, &cRsp1) - assert.NoError(t, err) - if cRsp1.User == nil { - t.Fatal("No user returned") - return - } - - var cRsp2 pb.CreateResponse - cReq2 := pb.CreateRequest{ - FirstName: "Barry", - LastName: "Doe", - Email: "barry@doe.com", - Password: "passwordabc", - } - err = h.Create(context.TODO(), &cReq2, &cRsp2) - assert.NoError(t, err) - if cRsp2.User == nil { - t.Fatal("No user returned") - return - } - - tt := []struct { - Name string - Token string - Time func() time.Time - Error error - User *pb.User - }{ - { - Name: "MissingToken", - Error: handler.ErrMissingToken, - }, - { - Name: "InvalidToken", - Error: handler.ErrInvalidToken, - Token: uuid.New().String(), - }, - { - Name: "ExpiredToken", - Error: handler.ErrTokenExpired, - Token: cRsp1.Token, - Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) }, - }, - { - Name: "ValidToken", - User: cRsp2.User, - Token: cRsp2.Token, - Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 3) }, - }, - { - Name: "RefreshedToken", - User: cRsp2.User, - Token: cRsp2.Token, - Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) }, - }, - } - - for _, tc := range tt { - t.Run(tc.Name, func(t *testing.T) { - if tc.Time == nil { - h.Time = time.Now - } else { - h.Time = tc.Time - } - - var rsp pb.ValidateResponse - err := h.Validate(context.TODO(), &pb.ValidateRequest{Token: tc.Token}, &rsp) - assert.Equal(t, tc.Error, err) - - if tc.User != nil { - assertUsersMatch(t, tc.User, rsp.User) - } else { - assert.Nil(t, tc.User) - } - }) - } -} - func assertUsersMatch(t *testing.T, exp, act *pb.User) { if act == nil { t.Error("No user returned") diff --git a/users/handler/list.go b/users/handler/list.go new file mode 100644 index 0000000..d4946c3 --- /dev/null +++ b/users/handler/list.go @@ -0,0 +1,26 @@ +package handler + +import ( + "context" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/users/proto" +) + +// List all users +func (u *Users) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error { + // query the database + var users []User + if err := u.DB.Model(&User{}).Find(&users).Error; err != nil { + logger.Errorf("Error reading from the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // serialize the response + rsp.Users = make([]*pb.User, len(users)) + for i, u := range users { + rsp.Users[i] = u.Serialize() + } + return nil +} diff --git a/users/handler/list_test.go b/users/handler/list_test.go new file mode 100644 index 0000000..f184c9c --- /dev/null +++ b/users/handler/list_test.go @@ -0,0 +1,67 @@ +package handler_test + +import ( + "context" + "testing" + + pb "github.com/micro/services/users/proto" + "github.com/stretchr/testify/assert" +) + +func TestList(t *testing.T) { + h := testHandler(t) + + // create some mock data + var cRsp1 pb.CreateResponse + cReq1 := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &cReq1, &cRsp1) + assert.NoError(t, err) + if cRsp1.User == nil { + t.Fatal("No user returned") + return + } + + var cRsp2 pb.CreateResponse + cReq2 := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "johndoe@gmail.com", + Password: "passwordabc", + } + err = h.Create(context.TODO(), &cReq2, &cRsp2) + assert.NoError(t, err) + if cRsp2.User == nil { + t.Fatal("No user returned") + return + } + + var rsp pb.ListResponse + err = h.List(context.TODO(), &pb.ListRequest{}, &rsp) + assert.NoError(t, err) + if rsp.Users == nil { + t.Error("No users returned") + return + } + + var u1Found, u2Found bool + for _, u := range rsp.Users { + switch u.Id { + case cRsp1.User.Id: + assertUsersMatch(t, cRsp1.User, u) + u1Found = true + case cRsp2.User.Id: + assertUsersMatch(t, cRsp2.User, u) + u2Found = true + default: + t.Fatal("Unexpected user returned") + return + } + } + assert.True(t, u1Found) + assert.True(t, u2Found) +} diff --git a/users/handler/login.go b/users/handler/login.go new file mode 100644 index 0000000..14b07fc --- /dev/null +++ b/users/handler/login.go @@ -0,0 +1,54 @@ +package handler + +import ( + "context" + + "github.com/google/uuid" + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/users/proto" + "gorm.io/gorm" +) + +// Login using email and password returns the users profile and a token +func (u *Users) Login(ctx context.Context, req *pb.LoginRequest, rsp *pb.LoginResponse) error { + // validate the request + if len(req.Email) == 0 { + return ErrMissingEmail + } + if len(req.Password) == 0 { + return ErrInvalidPassword + } + + return u.DB.Transaction(func(tx *gorm.DB) error { + // lookup the user + var user User + if err := tx.Where(&User{Email: req.Email}).First(&user).Error; err == gorm.ErrRecordNotFound { + return ErrNotFound + } else if err != nil { + logger.Errorf("Error reading from the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // compare the passwords + if !passwordsMatch(user.Password, req.Password) { + return ErrIncorrectPassword + } + + // generate a token for the user + token := Token{ + UserID: user.ID, + Key: uuid.New().String(), + ExpiresAt: u.Time().Add(tokenTTL), + } + if err := tx.Create(&token).Error; err != nil { + logger.Errorf("Error writing to the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // serialize the response + rsp.Token = token.Key + rsp.User = user.Serialize() + return nil + }) +} diff --git a/users/handler/login_test.go b/users/handler/login_test.go new file mode 100644 index 0000000..ca6ac1a --- /dev/null +++ b/users/handler/login_test.go @@ -0,0 +1,83 @@ +package handler_test + +import ( + "context" + "testing" + + "github.com/micro/services/users/handler" + pb "github.com/micro/services/users/proto" + "github.com/stretchr/testify/assert" +) + +func TestLogin(t *testing.T) { + h := testHandler(t) + + // create some mock data + var cRsp pb.CreateResponse + cReq := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &cReq, &cRsp) + assert.NoError(t, err) + if cRsp.User == nil { + t.Fatal("No user returned") + return + } + + tt := []struct { + Name string + Email string + Password string + Error error + User *pb.User + }{ + { + Name: "MissingEmail", + Password: "passwordabc", + Error: handler.ErrMissingEmail, + }, + { + Name: "MissingPassword", + Email: "john@doe.com", + Error: handler.ErrInvalidPassword, + }, + { + Name: "UserNotFound", + Email: "foo@bar.com", + Password: "passwordabc", + Error: handler.ErrNotFound, + }, + { + Name: "IncorrectPassword", + Email: "john@doe.com", + Password: "passwordabcdef", + Error: handler.ErrIncorrectPassword, + }, + { + Name: "Valid", + Email: "john@doe.com", + Password: "passwordabc", + User: cRsp.User, + }, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + var rsp pb.LoginResponse + err := h.Login(context.TODO(), &pb.LoginRequest{ + Email: tc.Email, Password: tc.Password, + }, &rsp) + assert.Equal(t, tc.Error, err) + + if tc.User != nil { + assertUsersMatch(t, tc.User, rsp.User) + assert.NotEmpty(t, rsp.Token) + } else { + assert.Nil(t, tc.User) + } + }) + } +} diff --git a/users/handler/logout.go b/users/handler/logout.go new file mode 100644 index 0000000..d3bafee --- /dev/null +++ b/users/handler/logout.go @@ -0,0 +1,37 @@ +package handler + +import ( + "context" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/users/proto" + "gorm.io/gorm" +) + +// Logout expires all tokens for the user +func (u *Users) Logout(ctx context.Context, req *pb.LogoutRequest, rsp *pb.LogoutResponse) error { + // validate the request + if len(req.Id) == 0 { + return ErrMissingID + } + + return u.DB.Transaction(func(tx *gorm.DB) error { + // lookup the user + var user User + if err := tx.Where(&User{ID: req.Id}).Preload("Tokens").First(&user).Error; err == gorm.ErrRecordNotFound { + return ErrNotFound + } else if err != nil { + logger.Errorf("Error reading from the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // delete the tokens + if err := tx.Delete(user.Tokens).Error; err != nil { + logger.Errorf("Error deleting from the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + return nil + }) +} diff --git a/users/handler/logout_test.go b/users/handler/logout_test.go new file mode 100644 index 0000000..fca369b --- /dev/null +++ b/users/handler/logout_test.go @@ -0,0 +1,48 @@ +package handler_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/micro/services/users/handler" + pb "github.com/micro/services/users/proto" + "github.com/stretchr/testify/assert" +) + +func TestLogout(t *testing.T) { + h := testHandler(t) + + t.Run("MissingUserID", func(t *testing.T) { + err := h.Logout(context.TODO(), &pb.LogoutRequest{}, &pb.LogoutResponse{}) + assert.Equal(t, handler.ErrMissingID, err) + }) + + t.Run("UserNotFound", func(t *testing.T) { + err := h.Logout(context.TODO(), &pb.LogoutRequest{Id: uuid.New().String()}, &pb.LogoutResponse{}) + assert.Equal(t, handler.ErrNotFound, err) + }) + + t.Run("Valid", func(t *testing.T) { + // create some mock data + var cRsp pb.CreateResponse + cReq := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &cReq, &cRsp) + assert.NoError(t, err) + if cRsp.User == nil { + t.Fatal("No user returned") + return + } + + err = h.Logout(context.TODO(), &pb.LogoutRequest{Id: cRsp.User.Id}, &pb.LogoutResponse{}) + assert.NoError(t, err) + + err = h.Validate(context.TODO(), &pb.ValidateRequest{Token: cRsp.Token}, &pb.ValidateResponse{}) + assert.Error(t, err) + }) +} diff --git a/users/handler/read.go b/users/handler/read.go new file mode 100644 index 0000000..9f941aa --- /dev/null +++ b/users/handler/read.go @@ -0,0 +1,31 @@ +package handler + +import ( + "context" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/users/proto" +) + +// Read users using ID +func (u *Users) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error { + // validate the request + if len(req.Ids) == 0 { + return ErrMissingIDs + } + + // query the database + var users []User + if err := u.DB.Model(&User{}).Where("id IN (?)", req.Ids).Find(&users).Error; err != nil { + logger.Errorf("Error reading from the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // serialize the response + rsp.Users = make(map[string]*pb.User, len(users)) + for _, u := range users { + rsp.Users[u.ID] = u.Serialize() + } + return nil +} diff --git a/users/handler/read_by_email.go b/users/handler/read_by_email.go new file mode 100644 index 0000000..773730d --- /dev/null +++ b/users/handler/read_by_email.go @@ -0,0 +1,36 @@ +package handler + +import ( + "context" + "strings" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/users/proto" +) + +// Read users using email +func (u *Users) ReadByEmail(ctx context.Context, req *pb.ReadByEmailRequest, rsp *pb.ReadByEmailResponse) error { + // validate the request + if len(req.Emails) == 0 { + return ErrMissingEmails + } + emails := make([]string, len(req.Emails)) + for i, e := range req.Emails { + emails[i] = strings.ToLower(e) + } + + // query the database + var users []User + if err := u.DB.Model(&User{}).Where("lower(email) IN (?)", emails).Find(&users).Error; err != nil { + logger.Errorf("Error reading from the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // serialize the response + rsp.Users = make(map[string]*pb.User, len(users)) + for _, u := range users { + rsp.Users[u.Email] = u.Serialize() + } + return nil +} diff --git a/users/handler/read_by_email_test.go b/users/handler/read_by_email_test.go new file mode 100644 index 0000000..1599262 --- /dev/null +++ b/users/handler/read_by_email_test.go @@ -0,0 +1,89 @@ +package handler_test + +import ( + "context" + "strings" + "testing" + + "github.com/micro/services/users/handler" + pb "github.com/micro/services/users/proto" + "github.com/stretchr/testify/assert" +) + +func TestReadByEmail(t *testing.T) { + h := testHandler(t) + + t.Run("MissingEmails", func(t *testing.T) { + var rsp pb.ReadByEmailResponse + err := h.ReadByEmail(context.TODO(), &pb.ReadByEmailRequest{}, &rsp) + assert.Equal(t, handler.ErrMissingEmails, err) + assert.Nil(t, rsp.Users) + }) + + t.Run("NotFound", func(t *testing.T) { + var rsp pb.ReadByEmailResponse + err := h.ReadByEmail(context.TODO(), &pb.ReadByEmailRequest{Emails: []string{"foo"}}, &rsp) + assert.Nil(t, err) + if rsp.Users == nil { + t.Fatal("Expected the users object to not be nil") + } + assert.Nil(t, rsp.Users["foo"]) + }) + + // create some mock data + var rsp1 pb.CreateResponse + req1 := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &req1, &rsp1) + assert.NoError(t, err) + if rsp1.User == nil { + t.Fatal("No user returned") + return + } + + var rsp2 pb.CreateResponse + req2 := pb.CreateRequest{ + FirstName: "Apple", + LastName: "Tree", + Email: "apple@tree.com", + Password: "passwordabc", + } + err = h.Create(context.TODO(), &req2, &rsp2) + assert.NoError(t, err) + if rsp2.User == nil { + t.Fatal("No user returned") + return + } + + // test the read + var rsp pb.ReadByEmailResponse + err = h.ReadByEmail(context.TODO(), &pb.ReadByEmailRequest{ + Emails: []string{rsp1.User.Email, strings.ToUpper(rsp2.User.Email)}, + }, &rsp) + assert.NoError(t, err) + + if rsp.Users == nil { + t.Fatal("Users not returned") + return + } + assert.NotNil(t, rsp.Users[rsp1.User.Email]) + assert.NotNil(t, rsp.Users[rsp2.User.Email]) + + // check the users match + if u := rsp.Users[rsp1.User.Email]; u != nil { + assert.Equal(t, rsp1.User.Id, u.Id) + assert.Equal(t, rsp1.User.FirstName, u.FirstName) + assert.Equal(t, rsp1.User.LastName, u.LastName) + assert.Equal(t, rsp1.User.Email, u.Email) + } + if u := rsp.Users[rsp2.User.Email]; u != nil { + assert.Equal(t, rsp2.User.Id, u.Id) + assert.Equal(t, rsp2.User.FirstName, u.FirstName) + assert.Equal(t, rsp2.User.LastName, u.LastName) + assert.Equal(t, rsp2.User.Email, u.Email) + } +} diff --git a/users/handler/read_test.go b/users/handler/read_test.go new file mode 100644 index 0000000..a10becb --- /dev/null +++ b/users/handler/read_test.go @@ -0,0 +1,88 @@ +package handler_test + +import ( + "context" + "testing" + + "github.com/micro/services/users/handler" + pb "github.com/micro/services/users/proto" + "github.com/stretchr/testify/assert" +) + +func TestRead(t *testing.T) { + h := testHandler(t) + + t.Run("MissingIDs", func(t *testing.T) { + var rsp pb.ReadResponse + err := h.Read(context.TODO(), &pb.ReadRequest{}, &rsp) + assert.Equal(t, handler.ErrMissingIDs, err) + assert.Nil(t, rsp.Users) + }) + + t.Run("NotFound", func(t *testing.T) { + var rsp pb.ReadResponse + err := h.Read(context.TODO(), &pb.ReadRequest{Ids: []string{"foo"}}, &rsp) + assert.Nil(t, err) + if rsp.Users == nil { + t.Fatal("Expected the users object to not be nil") + } + assert.Nil(t, rsp.Users["foo"]) + }) + + // create some mock data + var rsp1 pb.CreateResponse + req1 := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &req1, &rsp1) + assert.NoError(t, err) + if rsp1.User == nil { + t.Fatal("No user returned") + return + } + + var rsp2 pb.CreateResponse + req2 := pb.CreateRequest{ + FirstName: "Apple", + LastName: "Tree", + Email: "apple@tree.com", + Password: "passwordabc", + } + err = h.Create(context.TODO(), &req2, &rsp2) + assert.NoError(t, err) + if rsp2.User == nil { + t.Fatal("No user returned") + return + } + + // test the read + var rsp pb.ReadResponse + err = h.Read(context.TODO(), &pb.ReadRequest{ + Ids: []string{rsp1.User.Id, rsp2.User.Id}, + }, &rsp) + assert.NoError(t, err) + + if rsp.Users == nil { + t.Fatal("Users not returned") + return + } + assert.NotNil(t, rsp.Users[rsp1.User.Id]) + assert.NotNil(t, rsp.Users[rsp2.User.Id]) + + // check the users match + if u := rsp.Users[rsp1.User.Id]; u != nil { + assert.Equal(t, rsp1.User.Id, u.Id) + assert.Equal(t, rsp1.User.FirstName, u.FirstName) + assert.Equal(t, rsp1.User.LastName, u.LastName) + assert.Equal(t, rsp1.User.Email, u.Email) + } + if u := rsp.Users[rsp2.User.Id]; u != nil { + assert.Equal(t, rsp2.User.Id, u.Id) + assert.Equal(t, rsp2.User.FirstName, u.FirstName) + assert.Equal(t, rsp2.User.LastName, u.LastName) + assert.Equal(t, rsp2.User.Email, u.Email) + } +} diff --git a/users/handler/update.go b/users/handler/update.go new file mode 100644 index 0000000..71669a1 --- /dev/null +++ b/users/handler/update.go @@ -0,0 +1,75 @@ +package handler + +import ( + "context" + "strings" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/users/proto" + "gorm.io/gorm" +) + +// Update a user +func (u *Users) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error { + // validate the request + if len(req.Id) == 0 { + return ErrMissingID + } + if req.FirstName != nil && len(req.FirstName.Value) == 0 { + return ErrMissingFirstName + } + if req.LastName != nil && len(req.LastName.Value) == 0 { + return ErrMissingLastName + } + if req.Email != nil && len(req.Email.Value) == 0 { + return ErrMissingEmail + } + if req.Email != nil && !isEmailValid(req.Email.Value) { + return ErrInvalidEmail + } + if req.Password != nil && len(req.Password.Value) < 8 { + return ErrInvalidEmail + } + + // lookup the user + var user User + if err := u.DB.Where(&User{ID: req.Id}).First(&user).Error; err == gorm.ErrRecordNotFound { + return ErrNotFound + } else if err != nil { + logger.Errorf("Error reading from the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // assign the updated values + if req.FirstName != nil { + user.FirstName = req.FirstName.Value + } + if req.LastName != nil { + user.LastName = req.LastName.Value + } + if req.Email != nil { + user.Email = strings.ToLower(req.Email.Value) + } + if req.Password != nil { + p, err := hashAndSalt(req.Password.Value) + if err != nil { + logger.Errorf("Error hasing and salting password: %v", err) + return errors.InternalServerError("HASHING_ERROR", "Error hashing password") + } + user.Password = p + } + + // write the user to the database + err := u.DB.Save(user).Error + if err != nil && strings.Contains(err.Error(), "idx_users_email") { + return ErrDuplicateEmail + } else if err != nil { + logger.Errorf("Error writing to the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // serialize the user + rsp.User = user.Serialize() + return nil +} diff --git a/users/handler/update_test.go b/users/handler/update_test.go new file mode 100644 index 0000000..dd91775 --- /dev/null +++ b/users/handler/update_test.go @@ -0,0 +1,148 @@ +package handler_test + +import ( + "context" + "testing" + + "github.com/micro/services/users/handler" + pb "github.com/micro/services/users/proto" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func TestUpdate(t *testing.T) { + h := testHandler(t) + + t.Run("MissingID", func(t *testing.T) { + var rsp pb.UpdateResponse + err := h.Update(context.TODO(), &pb.UpdateRequest{}, &rsp) + assert.Equal(t, handler.ErrMissingID, err) + assert.Nil(t, rsp.User) + }) + + t.Run("NotFound", func(t *testing.T) { + var rsp pb.UpdateResponse + err := h.Update(context.TODO(), &pb.UpdateRequest{Id: "foo"}, &rsp) + assert.Equal(t, handler.ErrNotFound, err) + assert.Nil(t, rsp.User) + }) + + // create some mock data + var cRsp1 pb.CreateResponse + cReq1 := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &cReq1, &cRsp1) + assert.NoError(t, err) + if cRsp1.User == nil { + t.Fatal("No user returned") + return + } + + var cRsp2 pb.CreateResponse + cReq2 := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "johndoe@gmail.com", + Password: "passwordabc", + } + err = h.Create(context.TODO(), &cReq2, &cRsp2) + assert.NoError(t, err) + if cRsp2.User == nil { + t.Fatal("No user returned") + return + } + + t.Run("BlankFirstName", func(t *testing.T) { + var rsp pb.UpdateResponse + err := h.Update(context.TODO(), &pb.UpdateRequest{ + Id: cRsp1.User.Id, FirstName: &wrapperspb.StringValue{}, + }, &rsp) + assert.Equal(t, handler.ErrMissingFirstName, err) + assert.Nil(t, rsp.User) + }) + + t.Run("BlankLastName", func(t *testing.T) { + var rsp pb.UpdateResponse + err := h.Update(context.TODO(), &pb.UpdateRequest{ + Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{}, + }, &rsp) + assert.Equal(t, handler.ErrMissingLastName, err) + assert.Nil(t, rsp.User) + }) + + t.Run("BlankLastName", func(t *testing.T) { + var rsp pb.UpdateResponse + err := h.Update(context.TODO(), &pb.UpdateRequest{ + Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{}, + }, &rsp) + assert.Equal(t, handler.ErrMissingLastName, err) + assert.Nil(t, rsp.User) + }) + + t.Run("BlankEmail", func(t *testing.T) { + var rsp pb.UpdateResponse + err := h.Update(context.TODO(), &pb.UpdateRequest{ + Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{}, + }, &rsp) + assert.Equal(t, handler.ErrMissingEmail, err) + assert.Nil(t, rsp.User) + }) + + t.Run("InvalidEmail", func(t *testing.T) { + var rsp pb.UpdateResponse + err := h.Update(context.TODO(), &pb.UpdateRequest{ + Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: "foo.bar"}, + }, &rsp) + assert.Equal(t, handler.ErrInvalidEmail, err) + assert.Nil(t, rsp.User) + }) + + t.Run("EmailAlreadyExists", func(t *testing.T) { + var rsp pb.UpdateResponse + err := h.Update(context.TODO(), &pb.UpdateRequest{ + Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: cRsp2.User.Email}, + }, &rsp) + assert.Equal(t, handler.ErrDuplicateEmail, err) + assert.Nil(t, rsp.User) + }) + + t.Run("Valid", func(t *testing.T) { + uReq := pb.UpdateRequest{ + Id: cRsp1.User.Id, + Email: &wrapperspb.StringValue{Value: "foobar@gmail.com"}, + FirstName: &wrapperspb.StringValue{Value: "Foo"}, + LastName: &wrapperspb.StringValue{Value: "Bar"}, + } + var uRsp pb.UpdateResponse + err := h.Update(context.TODO(), &uReq, &uRsp) + assert.NoError(t, err) + if uRsp.User == nil { + t.Error("No user returned") + return + } + assert.Equal(t, cRsp1.User.Id, uRsp.User.Id) + assert.Equal(t, uReq.Email.Value, uRsp.User.Email) + assert.Equal(t, uReq.FirstName.Value, uRsp.User.FirstName) + assert.Equal(t, uReq.LastName.Value, uRsp.User.LastName) + }) + + t.Run("UpdatePassword", func(t *testing.T) { + uReq := pb.UpdateRequest{ + Id: cRsp2.User.Id, + Password: &wrapperspb.StringValue{Value: "helloworld"}, + } + err := h.Update(context.TODO(), &uReq, &pb.UpdateResponse{}) + assert.NoError(t, err) + + lReq := pb.LoginRequest{ + Email: cRsp2.User.Email, + Password: "helloworld", + } + err = h.Login(context.TODO(), &lReq, &pb.LoginResponse{}) + assert.NoError(t, err) + }) +} diff --git a/users/handler/validate.go b/users/handler/validate.go new file mode 100644 index 0000000..6efe75a --- /dev/null +++ b/users/handler/validate.go @@ -0,0 +1,45 @@ +package handler + +import ( + "context" + + "github.com/micro/micro/v3/service/errors" + "github.com/micro/micro/v3/service/logger" + pb "github.com/micro/services/users/proto" + "gorm.io/gorm" +) + +// Validate a token, each time a token is validated it extends its lifetime for another week +func (u *Users) Validate(ctx context.Context, req *pb.ValidateRequest, rsp *pb.ValidateResponse) error { + // validate the request + if len(req.Token) == 0 { + return ErrMissingToken + } + + return u.DB.Transaction(func(tx *gorm.DB) error { + // lookup the token + var token Token + if err := tx.Where(&Token{Key: req.Token}).Preload("User").First(&token).Error; err == gorm.ErrRecordNotFound { + return ErrInvalidToken + } else if err != nil { + logger.Errorf("Error reading from the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // ensure the token is valid + if u.Time().After(token.ExpiresAt) { + return ErrTokenExpired + } + + // extend the token for another lifetime + token.ExpiresAt = u.Time().Add(tokenTTL) + if err := tx.Save(&token).Error; err != nil { + logger.Errorf("Error writing to the database: %v", err) + return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database") + } + + // serialize the response + rsp.User = token.User.Serialize() + return nil + }) +} diff --git a/users/handler/validate_test.go b/users/handler/validate_test.go new file mode 100644 index 0000000..1a05c71 --- /dev/null +++ b/users/handler/validate_test.go @@ -0,0 +1,101 @@ +package handler_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/micro/services/users/handler" + pb "github.com/micro/services/users/proto" + "github.com/stretchr/testify/assert" +) + +func TestValidate(t *testing.T) { + h := testHandler(t) + + // create some mock data + var cRsp1 pb.CreateResponse + cReq1 := pb.CreateRequest{ + FirstName: "John", + LastName: "Doe", + Email: "john@doe.com", + Password: "passwordabc", + } + err := h.Create(context.TODO(), &cReq1, &cRsp1) + assert.NoError(t, err) + if cRsp1.User == nil { + t.Fatal("No user returned") + return + } + + var cRsp2 pb.CreateResponse + cReq2 := pb.CreateRequest{ + FirstName: "Barry", + LastName: "Doe", + Email: "barry@doe.com", + Password: "passwordabc", + } + err = h.Create(context.TODO(), &cReq2, &cRsp2) + assert.NoError(t, err) + if cRsp2.User == nil { + t.Fatal("No user returned") + return + } + + tt := []struct { + Name string + Token string + Time func() time.Time + Error error + User *pb.User + }{ + { + Name: "MissingToken", + Error: handler.ErrMissingToken, + }, + { + Name: "InvalidToken", + Error: handler.ErrInvalidToken, + Token: uuid.New().String(), + }, + { + Name: "ExpiredToken", + Error: handler.ErrTokenExpired, + Token: cRsp1.Token, + Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) }, + }, + { + Name: "ValidToken", + User: cRsp2.User, + Token: cRsp2.Token, + Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 3) }, + }, + { + Name: "RefreshedToken", + User: cRsp2.User, + Token: cRsp2.Token, + Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) }, + }, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + if tc.Time == nil { + h.Time = time.Now + } else { + h.Time = tc.Time + } + + var rsp pb.ValidateResponse + err := h.Validate(context.TODO(), &pb.ValidateRequest{Token: tc.Token}, &rsp) + assert.Equal(t, tc.Error, err) + + if tc.User != nil { + assertUsersMatch(t, tc.User, rsp.User) + } else { + assert.Nil(t, tc.User) + } + }) + } +} diff --git a/users/proto/users.pb.go b/users/proto/users.pb.go index 50f0dde..281e678 100644 --- a/users/proto/users.pb.go +++ b/users/proto/users.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.25.0 -// protoc v3.6.1 +// protoc-gen-go v1.23.0 +// protoc v3.13.0 // source: proto/users.proto package users @@ -317,6 +317,100 @@ func (x *ReadResponse) GetUsers() map[string]*User { return nil } +type ReadByEmailRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Emails []string `protobuf:"bytes,1,rep,name=emails,proto3" json:"emails,omitempty"` +} + +func (x *ReadByEmailRequest) Reset() { + *x = ReadByEmailRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_users_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReadByEmailRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadByEmailRequest) ProtoMessage() {} + +func (x *ReadByEmailRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_users_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadByEmailRequest.ProtoReflect.Descriptor instead. +func (*ReadByEmailRequest) Descriptor() ([]byte, []int) { + return file_proto_users_proto_rawDescGZIP(), []int{5} +} + +func (x *ReadByEmailRequest) GetEmails() []string { + if x != nil { + return x.Emails + } + return nil +} + +type ReadByEmailResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Users map[string]*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ReadByEmailResponse) Reset() { + *x = ReadByEmailResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_users_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReadByEmailResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadByEmailResponse) ProtoMessage() {} + +func (x *ReadByEmailResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_users_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadByEmailResponse.ProtoReflect.Descriptor instead. +func (*ReadByEmailResponse) Descriptor() ([]byte, []int) { + return file_proto_users_proto_rawDescGZIP(), []int{6} +} + +func (x *ReadByEmailResponse) GetUsers() map[string]*User { + if x != nil { + return x.Users + } + return nil +} + type UpdateRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -326,12 +420,13 @@ type UpdateRequest struct { FirstName *wrappers.StringValue `protobuf:"bytes,2,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` LastName *wrappers.StringValue `protobuf:"bytes,3,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` Email *wrappers.StringValue `protobuf:"bytes,4,opt,name=email,proto3" json:"email,omitempty"` + Password *wrappers.StringValue `protobuf:"bytes,5,opt,name=password,proto3" json:"password,omitempty"` } func (x *UpdateRequest) Reset() { *x = UpdateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[5] + mi := &file_proto_users_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -344,7 +439,7 @@ func (x *UpdateRequest) String() string { func (*UpdateRequest) ProtoMessage() {} func (x *UpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[5] + mi := &file_proto_users_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -357,7 +452,7 @@ func (x *UpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRequest.ProtoReflect.Descriptor instead. func (*UpdateRequest) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{5} + return file_proto_users_proto_rawDescGZIP(), []int{7} } func (x *UpdateRequest) GetId() string { @@ -388,6 +483,13 @@ func (x *UpdateRequest) GetEmail() *wrappers.StringValue { return nil } +func (x *UpdateRequest) GetPassword() *wrappers.StringValue { + if x != nil { + return x.Password + } + return nil +} + type UpdateResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -399,7 +501,7 @@ type UpdateResponse struct { func (x *UpdateResponse) Reset() { *x = UpdateResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[6] + mi := &file_proto_users_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -412,7 +514,7 @@ func (x *UpdateResponse) String() string { func (*UpdateResponse) ProtoMessage() {} func (x *UpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[6] + mi := &file_proto_users_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -425,7 +527,7 @@ func (x *UpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateResponse.ProtoReflect.Descriptor instead. func (*UpdateResponse) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{6} + return file_proto_users_proto_rawDescGZIP(), []int{8} } func (x *UpdateResponse) GetUser() *User { @@ -446,7 +548,7 @@ type DeleteRequest struct { func (x *DeleteRequest) Reset() { *x = DeleteRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[7] + mi := &file_proto_users_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -459,7 +561,7 @@ func (x *DeleteRequest) String() string { func (*DeleteRequest) ProtoMessage() {} func (x *DeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[7] + mi := &file_proto_users_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -472,7 +574,7 @@ func (x *DeleteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteRequest.ProtoReflect.Descriptor instead. func (*DeleteRequest) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{7} + return file_proto_users_proto_rawDescGZIP(), []int{9} } func (x *DeleteRequest) GetId() string { @@ -491,7 +593,7 @@ type DeleteResponse struct { func (x *DeleteResponse) Reset() { *x = DeleteResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[8] + mi := &file_proto_users_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -504,7 +606,7 @@ func (x *DeleteResponse) String() string { func (*DeleteResponse) ProtoMessage() {} func (x *DeleteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[8] + mi := &file_proto_users_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -517,7 +619,7 @@ func (x *DeleteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteResponse.ProtoReflect.Descriptor instead. func (*DeleteResponse) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{8} + return file_proto_users_proto_rawDescGZIP(), []int{10} } type ListRequest struct { @@ -529,7 +631,7 @@ type ListRequest struct { func (x *ListRequest) Reset() { *x = ListRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[9] + mi := &file_proto_users_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -542,7 +644,7 @@ func (x *ListRequest) String() string { func (*ListRequest) ProtoMessage() {} func (x *ListRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[9] + mi := &file_proto_users_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -555,7 +657,7 @@ func (x *ListRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRequest.ProtoReflect.Descriptor instead. func (*ListRequest) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{9} + return file_proto_users_proto_rawDescGZIP(), []int{11} } type ListResponse struct { @@ -569,7 +671,7 @@ type ListResponse struct { func (x *ListResponse) Reset() { *x = ListResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[10] + mi := &file_proto_users_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -582,7 +684,7 @@ func (x *ListResponse) String() string { func (*ListResponse) ProtoMessage() {} func (x *ListResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[10] + mi := &file_proto_users_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -595,7 +697,7 @@ func (x *ListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListResponse.ProtoReflect.Descriptor instead. func (*ListResponse) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{10} + return file_proto_users_proto_rawDescGZIP(), []int{12} } func (x *ListResponse) GetUsers() []*User { @@ -617,7 +719,7 @@ type LoginRequest struct { func (x *LoginRequest) Reset() { *x = LoginRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[11] + mi := &file_proto_users_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -630,7 +732,7 @@ func (x *LoginRequest) String() string { func (*LoginRequest) ProtoMessage() {} func (x *LoginRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[11] + mi := &file_proto_users_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -643,7 +745,7 @@ func (x *LoginRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead. func (*LoginRequest) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{11} + return file_proto_users_proto_rawDescGZIP(), []int{13} } func (x *LoginRequest) GetEmail() string { @@ -672,7 +774,7 @@ type LoginResponse struct { func (x *LoginResponse) Reset() { *x = LoginResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[12] + mi := &file_proto_users_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -685,7 +787,7 @@ func (x *LoginResponse) String() string { func (*LoginResponse) ProtoMessage() {} func (x *LoginResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[12] + mi := &file_proto_users_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -698,7 +800,7 @@ func (x *LoginResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead. func (*LoginResponse) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{12} + return file_proto_users_proto_rawDescGZIP(), []int{14} } func (x *LoginResponse) GetUser() *User { @@ -726,7 +828,7 @@ type LogoutRequest struct { func (x *LogoutRequest) Reset() { *x = LogoutRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[13] + mi := &file_proto_users_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -739,7 +841,7 @@ func (x *LogoutRequest) String() string { func (*LogoutRequest) ProtoMessage() {} func (x *LogoutRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[13] + mi := &file_proto_users_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -752,7 +854,7 @@ func (x *LogoutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead. func (*LogoutRequest) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{13} + return file_proto_users_proto_rawDescGZIP(), []int{15} } func (x *LogoutRequest) GetId() string { @@ -771,7 +873,7 @@ type LogoutResponse struct { func (x *LogoutResponse) Reset() { *x = LogoutResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[14] + mi := &file_proto_users_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -784,7 +886,7 @@ func (x *LogoutResponse) String() string { func (*LogoutResponse) ProtoMessage() {} func (x *LogoutResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[14] + mi := &file_proto_users_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -797,7 +899,7 @@ func (x *LogoutResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LogoutResponse.ProtoReflect.Descriptor instead. func (*LogoutResponse) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{14} + return file_proto_users_proto_rawDescGZIP(), []int{16} } type ValidateRequest struct { @@ -811,7 +913,7 @@ type ValidateRequest struct { func (x *ValidateRequest) Reset() { *x = ValidateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[15] + mi := &file_proto_users_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -824,7 +926,7 @@ func (x *ValidateRequest) String() string { func (*ValidateRequest) ProtoMessage() {} func (x *ValidateRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[15] + mi := &file_proto_users_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -837,7 +939,7 @@ func (x *ValidateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateRequest.ProtoReflect.Descriptor instead. func (*ValidateRequest) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{15} + return file_proto_users_proto_rawDescGZIP(), []int{17} } func (x *ValidateRequest) GetToken() string { @@ -858,7 +960,7 @@ type ValidateResponse struct { func (x *ValidateResponse) Reset() { *x = ValidateResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_users_proto_msgTypes[16] + mi := &file_proto_users_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -871,7 +973,7 @@ func (x *ValidateResponse) String() string { func (*ValidateResponse) ProtoMessage() {} func (x *ValidateResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_users_proto_msgTypes[16] + mi := &file_proto_users_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -884,7 +986,7 @@ func (x *ValidateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateResponse.ProtoReflect.Descriptor instead. func (*ValidateResponse) Descriptor() ([]byte, []int) { - return file_proto_users_proto_rawDescGZIP(), []int{16} + return file_proto_users_proto_rawDescGZIP(), []int{18} } func (x *ValidateResponse) GetUser() *User { @@ -930,78 +1032,98 @@ var file_proto_users_proto_rawDesc = []byte{ 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xcb, 0x01, 0x0a, 0x0d, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3b, 0x0a, - 0x0a, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x09, 0x66, 0x69, 0x72, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x09, 0x6c, 0x61, - 0x73, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6c, 0x61, 0x73, - 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x31, 0x0a, 0x0e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x75, - 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x0d, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x10, 0x0a, - 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x0d, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x31, - 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, - 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, - 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, - 0x73, 0x22, 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x22, 0x46, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x1f, 0x0a, 0x0d, 0x4c, - 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x10, 0x0a, 0x0e, - 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, - 0x0a, 0x0f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x33, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x75, - 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x32, 0xc6, 0x03, 0x0a, - 0x05, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x37, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x12, 0x14, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x12, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, - 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x37, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x75, - 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x06, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x75, - 0x73, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x13, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x12, 0x13, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, 0x0a, - 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x14, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, - 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, - 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x12, 0x16, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x0d, 0x5a, 0x0b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x75, - 0x73, 0x65, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x2c, 0x0a, 0x12, 0x52, + 0x65, 0x61, 0x64, 0x42, 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x06, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x99, 0x01, 0x0a, 0x13, 0x52, 0x65, + 0x61, 0x64, 0x42, 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x3b, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x25, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x42, 0x79, 0x45, + 0x6d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x55, 0x73, 0x65, + 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x1a, 0x45, + 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x85, 0x02, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3b, 0x0a, 0x0a, 0x66, 0x69, 0x72, 0x73, 0x74, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x66, 0x69, 0x72, 0x73, 0x74, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x32, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x65, 0x6d, + 0x61, 0x69, 0x6c, 0x12, 0x38, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x31, 0x0a, + 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1f, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x22, 0x1f, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x22, 0x10, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x31, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x21, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x0b, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, + 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x46, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, + 0x1f, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x22, 0x10, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x27, 0x0a, 0x0f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x33, 0x0a, 0x10, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1f, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x32, 0x8e, 0x04, 0x0a, 0x05, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x37, 0x0a, 0x06, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x12, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x13, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x42, 0x79, + 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x19, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, + 0x61, 0x64, 0x42, 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1a, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x42, 0x79, 0x45, + 0x6d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, + 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x12, 0x14, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x31, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x13, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x14, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x06, 0x4c, 0x6f, 0x67, + 0x6f, 0x75, 0x74, 0x12, 0x14, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, + 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1016,60 +1138,68 @@ func file_proto_users_proto_rawDescGZIP() []byte { return file_proto_users_proto_rawDescData } -var file_proto_users_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_proto_users_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_proto_users_proto_goTypes = []interface{}{ (*User)(nil), // 0: users.User (*CreateRequest)(nil), // 1: users.CreateRequest (*CreateResponse)(nil), // 2: users.CreateResponse (*ReadRequest)(nil), // 3: users.ReadRequest (*ReadResponse)(nil), // 4: users.ReadResponse - (*UpdateRequest)(nil), // 5: users.UpdateRequest - (*UpdateResponse)(nil), // 6: users.UpdateResponse - (*DeleteRequest)(nil), // 7: users.DeleteRequest - (*DeleteResponse)(nil), // 8: users.DeleteResponse - (*ListRequest)(nil), // 9: users.ListRequest - (*ListResponse)(nil), // 10: users.ListResponse - (*LoginRequest)(nil), // 11: users.LoginRequest - (*LoginResponse)(nil), // 12: users.LoginResponse - (*LogoutRequest)(nil), // 13: users.LogoutRequest - (*LogoutResponse)(nil), // 14: users.LogoutResponse - (*ValidateRequest)(nil), // 15: users.ValidateRequest - (*ValidateResponse)(nil), // 16: users.ValidateResponse - nil, // 17: users.ReadResponse.UsersEntry - (*wrappers.StringValue)(nil), // 18: google.protobuf.StringValue + (*ReadByEmailRequest)(nil), // 5: users.ReadByEmailRequest + (*ReadByEmailResponse)(nil), // 6: users.ReadByEmailResponse + (*UpdateRequest)(nil), // 7: users.UpdateRequest + (*UpdateResponse)(nil), // 8: users.UpdateResponse + (*DeleteRequest)(nil), // 9: users.DeleteRequest + (*DeleteResponse)(nil), // 10: users.DeleteResponse + (*ListRequest)(nil), // 11: users.ListRequest + (*ListResponse)(nil), // 12: users.ListResponse + (*LoginRequest)(nil), // 13: users.LoginRequest + (*LoginResponse)(nil), // 14: users.LoginResponse + (*LogoutRequest)(nil), // 15: users.LogoutRequest + (*LogoutResponse)(nil), // 16: users.LogoutResponse + (*ValidateRequest)(nil), // 17: users.ValidateRequest + (*ValidateResponse)(nil), // 18: users.ValidateResponse + nil, // 19: users.ReadResponse.UsersEntry + nil, // 20: users.ReadByEmailResponse.UsersEntry + (*wrappers.StringValue)(nil), // 21: google.protobuf.StringValue } var file_proto_users_proto_depIdxs = []int32{ 0, // 0: users.CreateResponse.user:type_name -> users.User - 17, // 1: users.ReadResponse.users:type_name -> users.ReadResponse.UsersEntry - 18, // 2: users.UpdateRequest.first_name:type_name -> google.protobuf.StringValue - 18, // 3: users.UpdateRequest.last_name:type_name -> google.protobuf.StringValue - 18, // 4: users.UpdateRequest.email:type_name -> google.protobuf.StringValue - 0, // 5: users.UpdateResponse.user:type_name -> users.User - 0, // 6: users.ListResponse.users:type_name -> users.User - 0, // 7: users.LoginResponse.user:type_name -> users.User - 0, // 8: users.ValidateResponse.user:type_name -> users.User - 0, // 9: users.ReadResponse.UsersEntry.value:type_name -> users.User - 1, // 10: users.Users.Create:input_type -> users.CreateRequest - 3, // 11: users.Users.Read:input_type -> users.ReadRequest - 5, // 12: users.Users.Update:input_type -> users.UpdateRequest - 7, // 13: users.Users.Delete:input_type -> users.DeleteRequest - 9, // 14: users.Users.List:input_type -> users.ListRequest - 11, // 15: users.Users.Login:input_type -> users.LoginRequest - 13, // 16: users.Users.Logout:input_type -> users.LogoutRequest - 15, // 17: users.Users.Validate:input_type -> users.ValidateRequest - 2, // 18: users.Users.Create:output_type -> users.CreateResponse - 4, // 19: users.Users.Read:output_type -> users.ReadResponse - 6, // 20: users.Users.Update:output_type -> users.UpdateResponse - 8, // 21: users.Users.Delete:output_type -> users.DeleteResponse - 10, // 22: users.Users.List:output_type -> users.ListResponse - 12, // 23: users.Users.Login:output_type -> users.LoginResponse - 14, // 24: users.Users.Logout:output_type -> users.LogoutResponse - 16, // 25: users.Users.Validate:output_type -> users.ValidateResponse - 18, // [18:26] is the sub-list for method output_type - 10, // [10:18] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 19, // 1: users.ReadResponse.users:type_name -> users.ReadResponse.UsersEntry + 20, // 2: users.ReadByEmailResponse.users:type_name -> users.ReadByEmailResponse.UsersEntry + 21, // 3: users.UpdateRequest.first_name:type_name -> google.protobuf.StringValue + 21, // 4: users.UpdateRequest.last_name:type_name -> google.protobuf.StringValue + 21, // 5: users.UpdateRequest.email:type_name -> google.protobuf.StringValue + 21, // 6: users.UpdateRequest.password:type_name -> google.protobuf.StringValue + 0, // 7: users.UpdateResponse.user:type_name -> users.User + 0, // 8: users.ListResponse.users:type_name -> users.User + 0, // 9: users.LoginResponse.user:type_name -> users.User + 0, // 10: users.ValidateResponse.user:type_name -> users.User + 0, // 11: users.ReadResponse.UsersEntry.value:type_name -> users.User + 0, // 12: users.ReadByEmailResponse.UsersEntry.value:type_name -> users.User + 1, // 13: users.Users.Create:input_type -> users.CreateRequest + 3, // 14: users.Users.Read:input_type -> users.ReadRequest + 5, // 15: users.Users.ReadByEmail:input_type -> users.ReadByEmailRequest + 7, // 16: users.Users.Update:input_type -> users.UpdateRequest + 9, // 17: users.Users.Delete:input_type -> users.DeleteRequest + 11, // 18: users.Users.List:input_type -> users.ListRequest + 13, // 19: users.Users.Login:input_type -> users.LoginRequest + 15, // 20: users.Users.Logout:input_type -> users.LogoutRequest + 17, // 21: users.Users.Validate:input_type -> users.ValidateRequest + 2, // 22: users.Users.Create:output_type -> users.CreateResponse + 4, // 23: users.Users.Read:output_type -> users.ReadResponse + 6, // 24: users.Users.ReadByEmail:output_type -> users.ReadByEmailResponse + 8, // 25: users.Users.Update:output_type -> users.UpdateResponse + 10, // 26: users.Users.Delete:output_type -> users.DeleteResponse + 12, // 27: users.Users.List:output_type -> users.ListResponse + 14, // 28: users.Users.Login:output_type -> users.LoginResponse + 16, // 29: users.Users.Logout:output_type -> users.LogoutResponse + 18, // 30: users.Users.Validate:output_type -> users.ValidateResponse + 22, // [22:31] is the sub-list for method output_type + 13, // [13:22] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_proto_users_proto_init() } @@ -1139,7 +1269,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateRequest); i { + switch v := v.(*ReadByEmailRequest); i { case 0: return &v.state case 1: @@ -1151,7 +1281,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateResponse); i { + switch v := v.(*ReadByEmailResponse); i { case 0: return &v.state case 1: @@ -1163,7 +1293,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteRequest); i { + switch v := v.(*UpdateRequest); i { case 0: return &v.state case 1: @@ -1175,7 +1305,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteResponse); i { + switch v := v.(*UpdateResponse); i { case 0: return &v.state case 1: @@ -1187,7 +1317,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListRequest); i { + switch v := v.(*DeleteRequest); i { case 0: return &v.state case 1: @@ -1199,7 +1329,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListResponse); i { + switch v := v.(*DeleteResponse); i { case 0: return &v.state case 1: @@ -1211,7 +1341,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginRequest); i { + switch v := v.(*ListRequest); i { case 0: return &v.state case 1: @@ -1223,7 +1353,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginResponse); i { + switch v := v.(*ListResponse); i { case 0: return &v.state case 1: @@ -1235,7 +1365,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LogoutRequest); i { + switch v := v.(*LoginRequest); i { case 0: return &v.state case 1: @@ -1247,7 +1377,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LogoutResponse); i { + switch v := v.(*LoginResponse); i { case 0: return &v.state case 1: @@ -1259,7 +1389,7 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateRequest); i { + switch v := v.(*LogoutRequest); i { case 0: return &v.state case 1: @@ -1271,6 +1401,30 @@ func file_proto_users_proto_init() { } } file_proto_users_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LogoutResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_users_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ValidateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_users_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ValidateResponse); i { case 0: return &v.state @@ -1289,7 +1443,7 @@ func file_proto_users_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_users_proto_rawDesc, NumEnums: 0, - NumMessages: 18, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, diff --git a/users/proto/users.pb.micro.go b/users/proto/users.pb.micro.go index 2aa58a3..4e9c565 100644 --- a/users/proto/users.pb.micro.go +++ b/users/proto/users.pb.micro.go @@ -45,6 +45,7 @@ func NewUsersEndpoints() []*api.Endpoint { type UsersService interface { Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error) Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) + ReadByEmail(ctx context.Context, in *ReadByEmailRequest, opts ...client.CallOption) (*ReadByEmailResponse, error) Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) @@ -88,6 +89,16 @@ func (c *usersService) Read(ctx context.Context, in *ReadRequest, opts ...client return out, nil } +func (c *usersService) ReadByEmail(ctx context.Context, in *ReadByEmailRequest, opts ...client.CallOption) (*ReadByEmailResponse, error) { + req := c.c.NewRequest(c.name, "Users.ReadByEmail", in) + out := new(ReadByEmailResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *usersService) Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error) { req := c.c.NewRequest(c.name, "Users.Update", in) out := new(UpdateResponse) @@ -153,6 +164,7 @@ func (c *usersService) Validate(ctx context.Context, in *ValidateRequest, opts . type UsersHandler interface { Create(context.Context, *CreateRequest, *CreateResponse) error Read(context.Context, *ReadRequest, *ReadResponse) error + ReadByEmail(context.Context, *ReadByEmailRequest, *ReadByEmailResponse) error Update(context.Context, *UpdateRequest, *UpdateResponse) error Delete(context.Context, *DeleteRequest, *DeleteResponse) error List(context.Context, *ListRequest, *ListResponse) error @@ -168,6 +180,7 @@ func RegisterUsersHandler(s server.Server, hdlr UsersHandler, opts ...server.Han type users interface { Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error + ReadByEmail(ctx context.Context, in *ReadByEmailRequest, out *ReadByEmailResponse) error Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error List(ctx context.Context, in *ListRequest, out *ListResponse) error @@ -194,6 +207,10 @@ func (h *usersHandler) Read(ctx context.Context, in *ReadRequest, out *ReadRespo return h.UsersHandler.Read(ctx, in, out) } +func (h *usersHandler) ReadByEmail(ctx context.Context, in *ReadByEmailRequest, out *ReadByEmailResponse) error { + return h.UsersHandler.ReadByEmail(ctx, in, out) +} + func (h *usersHandler) Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error { return h.UsersHandler.Update(ctx, in, out) } diff --git a/users/proto/users.proto b/users/proto/users.proto index c96a2ef..9a9f938 100644 --- a/users/proto/users.proto +++ b/users/proto/users.proto @@ -7,6 +7,7 @@ import "google/protobuf/wrappers.proto"; service Users { rpc Create(CreateRequest) returns (CreateResponse) {} rpc Read(ReadRequest) returns (ReadResponse) {} + rpc ReadByEmail(ReadByEmailRequest) returns (ReadByEmailResponse) {} rpc Update(UpdateRequest) returns (UpdateResponse) {} rpc Delete(DeleteRequest) returns (DeleteResponse) {} rpc List(ListRequest) returns (ListResponse) {} @@ -46,11 +47,20 @@ message ReadResponse { map users = 1; } +message ReadByEmailRequest { + repeated string emails = 1; +} + +message ReadByEmailResponse { + map users = 1; +} + message UpdateRequest { string id = 1; google.protobuf.StringValue first_name = 2; google.protobuf.StringValue last_name = 3; google.protobuf.StringValue email = 4; + google.protobuf.StringValue password = 5; } message UpdateResponse {