From b914f3b4f66a635a2d5398a611a3f125548b663f Mon Sep 17 00:00:00 2001 From: WashingtonKK Date: Fri, 31 Oct 2025 14:37:22 +0300 Subject: [PATCH 1/9] Create email gRPC endpoint Signed-off-by: WashingtonKK --- api/grpc/auth/v1/auth.pb.go | 2 +- api/grpc/auth/v1/auth_grpc.pb.go | 2 +- api/grpc/channels/v1/channels.pb.go | 2 +- api/grpc/channels/v1/channels_grpc.pb.go | 2 +- api/grpc/clients/v1/clients.pb.go | 2 +- api/grpc/clients/v1/clients_grpc.pb.go | 2 +- api/grpc/common/v1/common.pb.go | 2 +- api/grpc/domains/v1/domains.pb.go | 2 +- api/grpc/domains/v1/domains_grpc.pb.go | 2 +- api/grpc/groups/v1/groups.pb.go | 2 +- api/grpc/groups/v1/groups_grpc.pb.go | 2 +- api/grpc/token/v1/token.pb.go | 2 +- api/grpc/token/v1/token_grpc.pb.go | 2 +- api/grpc/users/v1/users.pb.go | 230 +++++++++++++++++++++++ api/grpc/users/v1/users_grpc.pb.go | 130 +++++++++++++ cmd/domains/main.go | 17 ++ cmd/users/main.go | 25 ++- docker/.env | 13 ++ docker/docker-compose.yaml | 13 ++ docker/templates/invitation.tmpl | 13 ++ internal/proto/users/v1/users.proto | 27 +++ pkg/grpcclient/client.go | 24 +++ pkg/messaging/message.pb.go | 2 +- users/api/grpc/client.go | 92 +++++++++ users/api/grpc/endpoint.go | 51 +++++ users/api/grpc/server.go | 57 ++++++ users/emailer.go | 3 + users/emailer/emailer.go | 5 + users/events/streams.go | 4 + users/middleware/authorization.go | 4 + users/middleware/logging.go | 19 ++ users/middleware/metrics.go | 9 + users/middleware/tracing.go | 11 ++ users/mocks/emailer.go | 87 +++++++++ users/mocks/service.go | 93 +++++++++ users/service.go | 4 + users/users.go | 3 + 37 files changed, 947 insertions(+), 15 deletions(-) create mode 100644 api/grpc/users/v1/users.pb.go create mode 100644 api/grpc/users/v1/users_grpc.pb.go create mode 100644 docker/templates/invitation.tmpl create mode 100644 internal/proto/users/v1/users.proto create mode 100644 users/api/grpc/client.go create mode 100644 users/api/grpc/endpoint.go create mode 100644 users/api/grpc/server.go diff --git a/api/grpc/auth/v1/auth.pb.go b/api/grpc/auth/v1/auth.pb.go index c71ceb3efc..18e2e08446 100644 --- a/api/grpc/auth/v1/auth.pb.go +++ b/api/grpc/auth/v1/auth.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc v5.29.0 // source: auth/v1/auth.proto package v1 diff --git a/api/grpc/auth/v1/auth_grpc.pb.go b/api/grpc/auth/v1/auth_grpc.pb.go index dbeeab1449..d85fabae7c 100644 --- a/api/grpc/auth/v1/auth_grpc.pb.go +++ b/api/grpc/auth/v1/auth_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.33.0 +// - protoc v5.29.0 // source: auth/v1/auth.proto package v1 diff --git a/api/grpc/channels/v1/channels.pb.go b/api/grpc/channels/v1/channels.pb.go index 94b4f230c3..22783fff04 100644 --- a/api/grpc/channels/v1/channels.pb.go +++ b/api/grpc/channels/v1/channels.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc v5.29.0 // source: channels/v1/channels.proto package v1 diff --git a/api/grpc/channels/v1/channels_grpc.pb.go b/api/grpc/channels/v1/channels_grpc.pb.go index aec78d4a4d..9b9fe58856 100644 --- a/api/grpc/channels/v1/channels_grpc.pb.go +++ b/api/grpc/channels/v1/channels_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.33.0 +// - protoc v5.29.0 // source: channels/v1/channels.proto package v1 diff --git a/api/grpc/clients/v1/clients.pb.go b/api/grpc/clients/v1/clients.pb.go index 3fa2d5d73c..fd416e5f47 100644 --- a/api/grpc/clients/v1/clients.pb.go +++ b/api/grpc/clients/v1/clients.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc v5.29.0 // source: clients/v1/clients.proto package v1 diff --git a/api/grpc/clients/v1/clients_grpc.pb.go b/api/grpc/clients/v1/clients_grpc.pb.go index e6b66f08af..bd04a8c47f 100644 --- a/api/grpc/clients/v1/clients_grpc.pb.go +++ b/api/grpc/clients/v1/clients_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.33.0 +// - protoc v5.29.0 // source: clients/v1/clients.proto package v1 diff --git a/api/grpc/common/v1/common.pb.go b/api/grpc/common/v1/common.pb.go index 18954ab2cc..c1a24e5d73 100644 --- a/api/grpc/common/v1/common.pb.go +++ b/api/grpc/common/v1/common.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc v5.29.0 // source: common/v1/common.proto package v1 diff --git a/api/grpc/domains/v1/domains.pb.go b/api/grpc/domains/v1/domains.pb.go index ac09207971..c51f9bd832 100644 --- a/api/grpc/domains/v1/domains.pb.go +++ b/api/grpc/domains/v1/domains.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc v5.29.0 // source: domains/v1/domains.proto package v1 diff --git a/api/grpc/domains/v1/domains_grpc.pb.go b/api/grpc/domains/v1/domains_grpc.pb.go index 0fcf6e4d9a..0171109a87 100644 --- a/api/grpc/domains/v1/domains_grpc.pb.go +++ b/api/grpc/domains/v1/domains_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.33.0 +// - protoc v5.29.0 // source: domains/v1/domains.proto package v1 diff --git a/api/grpc/groups/v1/groups.pb.go b/api/grpc/groups/v1/groups.pb.go index f62ab9e450..6b4fd65f0d 100644 --- a/api/grpc/groups/v1/groups.pb.go +++ b/api/grpc/groups/v1/groups.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc v5.29.0 // source: groups/v1/groups.proto package v1 diff --git a/api/grpc/groups/v1/groups_grpc.pb.go b/api/grpc/groups/v1/groups_grpc.pb.go index e742a317cd..d362f88c2b 100644 --- a/api/grpc/groups/v1/groups_grpc.pb.go +++ b/api/grpc/groups/v1/groups_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.33.0 +// - protoc v5.29.0 // source: groups/v1/groups.proto package v1 diff --git a/api/grpc/token/v1/token.pb.go b/api/grpc/token/v1/token.pb.go index 937ee5fee4..3e75135363 100644 --- a/api/grpc/token/v1/token.pb.go +++ b/api/grpc/token/v1/token.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc v5.29.0 // source: token/v1/token.proto package v1 diff --git a/api/grpc/token/v1/token_grpc.pb.go b/api/grpc/token/v1/token_grpc.pb.go index 70ac6a7609..f3adacfb70 100644 --- a/api/grpc/token/v1/token_grpc.pb.go +++ b/api/grpc/token/v1/token_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.33.0 +// - protoc v5.29.0 // source: token/v1/token.proto package v1 diff --git a/api/grpc/users/v1/users.pb.go b/api/grpc/users/v1/users.pb.go new file mode 100644 index 0000000000..879c699f95 --- /dev/null +++ b/api/grpc/users/v1/users.pb.go @@ -0,0 +1,230 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v5.29.0 +// source: users/v1/users.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SendEmailReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + To []string `protobuf:"bytes,1,rep,name=to,proto3" json:"to,omitempty"` // Email recipients + From string `protobuf:"bytes,2,opt,name=from,proto3" json:"from,omitempty"` // Sender email address (optional) + Subject string `protobuf:"bytes,3,opt,name=subject,proto3" json:"subject,omitempty"` // Email subject + Header string `protobuf:"bytes,4,opt,name=header,proto3" json:"header,omitempty"` // Email header text + User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` // User name + Content string `protobuf:"bytes,6,opt,name=content,proto3" json:"content,omitempty"` // Email content/URL + Footer string `protobuf:"bytes,7,opt,name=footer,proto3" json:"footer,omitempty"` // Email footer text + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendEmailReq) Reset() { + *x = SendEmailReq{} + mi := &file_users_v1_users_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendEmailReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendEmailReq) ProtoMessage() {} + +func (x *SendEmailReq) ProtoReflect() protoreflect.Message { + mi := &file_users_v1_users_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendEmailReq.ProtoReflect.Descriptor instead. +func (*SendEmailReq) Descriptor() ([]byte, []int) { + return file_users_v1_users_proto_rawDescGZIP(), []int{0} +} + +func (x *SendEmailReq) GetTo() []string { + if x != nil { + return x.To + } + return nil +} + +func (x *SendEmailReq) GetFrom() string { + if x != nil { + return x.From + } + return "" +} + +func (x *SendEmailReq) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *SendEmailReq) GetHeader() string { + if x != nil { + return x.Header + } + return "" +} + +func (x *SendEmailReq) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *SendEmailReq) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *SendEmailReq) GetFooter() string { + if x != nil { + return x.Footer + } + return "" +} + +type SendEmailRes struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sent bool `protobuf:"varint,1,opt,name=sent,proto3" json:"sent,omitempty"` // Whether the email was sent successfully + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendEmailRes) Reset() { + *x = SendEmailRes{} + mi := &file_users_v1_users_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendEmailRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendEmailRes) ProtoMessage() {} + +func (x *SendEmailRes) ProtoReflect() protoreflect.Message { + mi := &file_users_v1_users_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendEmailRes.ProtoReflect.Descriptor instead. +func (*SendEmailRes) Descriptor() ([]byte, []int) { + return file_users_v1_users_proto_rawDescGZIP(), []int{1} +} + +func (x *SendEmailRes) GetSent() bool { + if x != nil { + return x.Sent + } + return false +} + +var File_users_v1_users_proto protoreflect.FileDescriptor + +const file_users_v1_users_proto_rawDesc = "" + + "\n" + + "\x14users/v1/users.proto\x12\busers.v1\"\xaa\x01\n" + + "\fSendEmailReq\x12\x0e\n" + + "\x02to\x18\x01 \x03(\tR\x02to\x12\x12\n" + + "\x04from\x18\x02 \x01(\tR\x04from\x12\x18\n" + + "\asubject\x18\x03 \x01(\tR\asubject\x12\x16\n" + + "\x06header\x18\x04 \x01(\tR\x06header\x12\x12\n" + + "\x04user\x18\x05 \x01(\tR\x04user\x12\x18\n" + + "\acontent\x18\x06 \x01(\tR\acontent\x12\x16\n" + + "\x06footer\x18\a \x01(\tR\x06footer\"\"\n" + + "\fSendEmailRes\x12\x12\n" + + "\x04sent\x18\x01 \x01(\bR\x04sent2M\n" + + "\fUsersService\x12=\n" + + "\tSendEmail\x12\x16.users.v1.SendEmailReq\x1a\x16.users.v1.SendEmailRes\"\x00B.Z,github.com/absmach/supermq/api/grpc/users/v1b\x06proto3" + +var ( + file_users_v1_users_proto_rawDescOnce sync.Once + file_users_v1_users_proto_rawDescData []byte +) + +func file_users_v1_users_proto_rawDescGZIP() []byte { + file_users_v1_users_proto_rawDescOnce.Do(func() { + file_users_v1_users_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_users_v1_users_proto_rawDesc), len(file_users_v1_users_proto_rawDesc))) + }) + return file_users_v1_users_proto_rawDescData +} + +var file_users_v1_users_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_users_v1_users_proto_goTypes = []any{ + (*SendEmailReq)(nil), // 0: users.v1.SendEmailReq + (*SendEmailRes)(nil), // 1: users.v1.SendEmailRes +} +var file_users_v1_users_proto_depIdxs = []int32{ + 0, // 0: users.v1.UsersService.SendEmail:input_type -> users.v1.SendEmailReq + 1, // 1: users.v1.UsersService.SendEmail:output_type -> users.v1.SendEmailRes + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_users_v1_users_proto_init() } +func file_users_v1_users_proto_init() { + if File_users_v1_users_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_users_v1_users_proto_rawDesc), len(file_users_v1_users_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_users_v1_users_proto_goTypes, + DependencyIndexes: file_users_v1_users_proto_depIdxs, + MessageInfos: file_users_v1_users_proto_msgTypes, + }.Build() + File_users_v1_users_proto = out.File + file_users_v1_users_proto_goTypes = nil + file_users_v1_users_proto_depIdxs = nil +} diff --git a/api/grpc/users/v1/users_grpc.pb.go b/api/grpc/users/v1/users_grpc.pb.go new file mode 100644 index 0000000000..7417e78553 --- /dev/null +++ b/api/grpc/users/v1/users_grpc.pb.go @@ -0,0 +1,130 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.0 +// source: users/v1/users.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + UsersService_SendEmail_FullMethodName = "/users.v1.UsersService/SendEmail" +) + +// UsersServiceClient is the client API for UsersService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// UsersService is a service that provides user-related functionalities +// for SuperMQ services. +type UsersServiceClient interface { + SendEmail(ctx context.Context, in *SendEmailReq, opts ...grpc.CallOption) (*SendEmailRes, error) +} + +type usersServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewUsersServiceClient(cc grpc.ClientConnInterface) UsersServiceClient { + return &usersServiceClient{cc} +} + +func (c *usersServiceClient) SendEmail(ctx context.Context, in *SendEmailReq, opts ...grpc.CallOption) (*SendEmailRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SendEmailRes) + err := c.cc.Invoke(ctx, UsersService_SendEmail_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// UsersServiceServer is the server API for UsersService service. +// All implementations must embed UnimplementedUsersServiceServer +// for forward compatibility. +// +// UsersService is a service that provides user-related functionalities +// for SuperMQ services. +type UsersServiceServer interface { + SendEmail(context.Context, *SendEmailReq) (*SendEmailRes, error) + mustEmbedUnimplementedUsersServiceServer() +} + +// UnimplementedUsersServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedUsersServiceServer struct{} + +func (UnimplementedUsersServiceServer) SendEmail(context.Context, *SendEmailReq) (*SendEmailRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendEmail not implemented") +} +func (UnimplementedUsersServiceServer) mustEmbedUnimplementedUsersServiceServer() {} +func (UnimplementedUsersServiceServer) testEmbeddedByValue() {} + +// UnsafeUsersServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to UsersServiceServer will +// result in compilation errors. +type UnsafeUsersServiceServer interface { + mustEmbedUnimplementedUsersServiceServer() +} + +func RegisterUsersServiceServer(s grpc.ServiceRegistrar, srv UsersServiceServer) { + // If the following call pancis, it indicates UnimplementedUsersServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&UsersService_ServiceDesc, srv) +} + +func _UsersService_SendEmail_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendEmailReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UsersServiceServer).SendEmail(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UsersService_SendEmail_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UsersServiceServer).SendEmail(ctx, req.(*SendEmailReq)) + } + return interceptor(ctx, in, info, handler) +} + +// UsersService_ServiceDesc is the grpc.ServiceDesc for UsersService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var UsersService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "users.v1.UsersService", + HandlerType: (*UsersServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SendEmail", + Handler: _UsersService_SendEmail_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "users/v1/users.proto", +} diff --git a/cmd/domains/main.go b/cmd/domains/main.go index 08da6cb156..040f8b3cca 100644 --- a/cmd/domains/main.go +++ b/cmd/domains/main.go @@ -63,6 +63,7 @@ const ( envPrefixGrpc = "SMQ_DOMAINS_GRPC_" envPrefixDB = "SMQ_DOMAINS_DB_" envPrefixAuth = "SMQ_AUTH_GRPC_" + envPrefixUsers = "SMQ_USERS_GRPC_" envPrefixDomainCallout = "SMQ_DOMAINS_CALLOUT_" defDB = "domains" defSvcHTTPPort = "9004" @@ -161,6 +162,22 @@ func main() { logger.Info("Authn successfully connected to auth gRPC server " + authnHandler.Secure()) authnMiddleware := smqauthn.NewAuthNMiddleware(authn) + usersClientConfig := grpcclient.Config{} + if err := env.ParseWithOptions(&usersClientConfig, env.Options{Prefix: envPrefixUsers}); err != nil { + logger.Error(fmt.Sprintf("failed to load users gRPC client configuration : %s", err)) + exitCode = 1 + return + } + + _, usersHandler, err := grpcclient.SetupUsersClient(ctx, usersClientConfig) + if err != nil { + logger.Error(fmt.Sprintf("failed to connect to users gRPC server : %s", err.Error())) + exitCode = 1 + return + } + defer usersHandler.Close() + logger.Info("Users client successfully connected to users gRPC server " + usersHandler.Secure()) + database := postgres.NewDatabase(db, dbConfig, tracer) domainsRepo := dpostgres.NewRepository(database) diff --git a/cmd/users/main.go b/cmd/users/main.go index 4f9c08e50a..c99b50f565 100644 --- a/cmd/users/main.go +++ b/cmd/users/main.go @@ -18,6 +18,7 @@ import ( "github.com/absmach/supermq" grpcDomainsV1 "github.com/absmach/supermq/api/grpc/domains/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" + grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" "github.com/absmach/supermq/internal/email" smqlog "github.com/absmach/supermq/logger" smqauthn "github.com/absmach/supermq/pkg/authn" @@ -35,10 +36,12 @@ import ( pgclient "github.com/absmach/supermq/pkg/postgres" "github.com/absmach/supermq/pkg/prometheus" "github.com/absmach/supermq/pkg/server" + grpcserver "github.com/absmach/supermq/pkg/server/grpc" httpserver "github.com/absmach/supermq/pkg/server/http" "github.com/absmach/supermq/pkg/uuid" "github.com/absmach/supermq/users" httpapi "github.com/absmach/supermq/users/api" + usersgrpcapi "github.com/absmach/supermq/users/api/grpc" "github.com/absmach/supermq/users/emailer" "github.com/absmach/supermq/users/events" "github.com/absmach/supermq/users/hasher" @@ -53,17 +56,20 @@ import ( "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/reflection" ) const ( svcName = "users" envPrefixDB = "SMQ_USERS_DB_" envPrefixHTTP = "SMQ_USERS_HTTP_" + envPrefixGRPC = "SMQ_USERS_GRPC_" envPrefixAuth = "SMQ_AUTH_GRPC_" envPrefixDomains = "SMQ_DOMAINS_GRPC_" envPrefixGoogle = "SMQ_GOOGLE_" defDB = "users" defSvcHTTPPort = "9002" + defSvcGRPCPort = "7002" ) type config struct { @@ -234,6 +240,19 @@ func main() { return } + grpcServerConfig := server.Config{Port: defSvcGRPCPort} + if err := env.ParseWithOptions(&grpcServerConfig, env.Options{Prefix: envPrefixGRPC}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s gRPC server configuration : %s", svcName, err.Error())) + exitCode = 1 + return + } + registerUsersServiceServer := func(srv *grpc.Server) { + reflection.Register(srv) + grpcUsersV1.RegisterUsersServiceServer(srv, usersgrpcapi.NewUsersServer(csvc)) + } + + gs := grpcserver.NewServer(ctx, cancel, svcName, grpcServerConfig, registerUsersServiceServer, logger) + httpServerConfig := server.Config{Port: defSvcHTTPPort} if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err.Error())) @@ -258,12 +277,16 @@ func main() { go chc.CallHome(ctx) } + g.Go(func() error { + return gs.Start() + }) + g.Go(func() error { return httpSrv.Start() }) g.Go(func() error { - return server.StopSignalHandler(ctx, cancel, logger, svcName, httpSrv) + return server.StopSignalHandler(ctx, cancel, logger, svcName, httpSrv, gs) }) if err := g.Wait(); err != nil { diff --git a/docker/.env b/docker/.env index 747b0e0a5c..8113d422ef 100644 --- a/docker/.env +++ b/docker/.env @@ -154,6 +154,13 @@ SMQ_AUTH_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt} SMQ_AUTH_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key} SMQ_AUTH_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt} +#### Users gRPC Client Config +SMQ_USERS_GRPC_URL=users:7002 +SMQ_USERS_GRPC_TIMEOUT=300s +SMQ_USERS_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/users-grpc-client.crt} +SMQ_USERS_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/users-grpc-client.key} +SMQ_USERS_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt} + ### Domains SMQ_DOMAINS_LOG_LEVEL=debug SMQ_DOMAINS_HTTP_HOST=domains @@ -239,6 +246,12 @@ SMQ_USERS_HTTP_HOST=users SMQ_USERS_HTTP_PORT=9002 SMQ_USERS_HTTP_SERVER_CERT= SMQ_USERS_HTTP_SERVER_KEY= +SMQ_USERS_GRPC_HOST=users +SMQ_USERS_GRPC_PORT=7002 +SMQ_USERS_GRPC_SERVER_CERT= +SMQ_USERS_GRPC_SERVER_KEY= +SMQ_USERS_GRPC_SERVER_CA_CERTS= +SMQ_USERS_GRPC_CLIENT_CA_CERTS= SMQ_USERS_DB_HOST=users-db SMQ_USERS_DB_PORT=5432 SMQ_USERS_DB_USER=supermq diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 3a04f7c4f9..fda0a53af6 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -258,6 +258,11 @@ services: SMQ_AUTH_GRPC_CLIENT_CERT: ${SMQ_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt} SMQ_AUTH_GRPC_CLIENT_KEY: ${SMQ_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key} SMQ_AUTH_GRPC_SERVER_CA_CERTS: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt} + SMQ_USERS_GRPC_URL: ${SMQ_USERS_GRPC_URL} + SMQ_USERS_GRPC_TIMEOUT: ${SMQ_USERS_GRPC_TIMEOUT} + SMQ_USERS_GRPC_CLIENT_CERT: ${SMQ_USERS_GRPC_CLIENT_CERT:+/users-grpc-client.crt} + SMQ_USERS_GRPC_CLIENT_KEY: ${SMQ_USERS_GRPC_CLIENT_KEY:+/users-grpc-client.key} + SMQ_USERS_GRPC_SERVER_CA_CERTS: ${SMQ_USERS_GRPC_SERVER_CA_CERTS:+/users-grpc-server-ca.crt} SMQ_GROUPS_GRPC_URL: ${SMQ_GROUPS_GRPC_URL} SMQ_GROUPS_GRPC_TIMEOUT: ${SMQ_GROUPS_GRPC_TIMEOUT} SMQ_GROUPS_GRPC_CLIENT_CERT: ${SMQ_GROUPS_GRPC_CLIENT_CERT:+/groups-grpc-client.crt} @@ -830,6 +835,8 @@ services: - users-db - auth - nats + expose: + - ${SMQ_USERS_GRPC_PORT} restart: on-failure environment: SMQ_USERS_LOG_LEVEL: ${SMQ_USERS_LOG_LEVEL} @@ -846,6 +853,12 @@ services: SMQ_USERS_HTTP_PORT: ${SMQ_USERS_HTTP_PORT} SMQ_USERS_HTTP_SERVER_CERT: ${SMQ_USERS_HTTP_SERVER_CERT} SMQ_USERS_HTTP_SERVER_KEY: ${SMQ_USERS_HTTP_SERVER_KEY} + SMQ_USERS_GRPC_HOST: ${SMQ_USERS_GRPC_HOST} + SMQ_USERS_GRPC_PORT: ${SMQ_USERS_GRPC_PORT} + SMQ_USERS_GRPC_SERVER_CERT: ${SMQ_USERS_GRPC_SERVER_CERT:+/users-grpc-server.crt} + SMQ_USERS_GRPC_SERVER_KEY: ${SMQ_USERS_GRPC_SERVER_KEY:+/users-grpc-server.key} + SMQ_USERS_GRPC_SERVER_CA_CERTS: ${SMQ_USERS_GRPC_SERVER_CA_CERTS:+/users-grpc-server-ca.crt} + SMQ_USERS_GRPC_CLIENT_CA_CERTS: ${SMQ_USERS_GRPC_CLIENT_CA_CERTS:+/users-grpc-client-ca.crt} SMQ_USERS_DB_HOST: ${SMQ_USERS_DB_HOST} SMQ_USERS_DB_PORT: ${SMQ_USERS_DB_PORT} SMQ_USERS_DB_USER: ${SMQ_USERS_DB_USER} diff --git a/docker/templates/invitation.tmpl b/docker/templates/invitation.tmpl new file mode 100644 index 0000000000..efba594d21 --- /dev/null +++ b/docker/templates/invitation.tmpl @@ -0,0 +1,13 @@ +Dear {{.User}}, + +Welcome to {{.Host}}! You have been invited to join a workspace on our platform. To complete your registration, please click on the link below to log in to your account and accept the invitation: + +{{.Content}} + +The invitation will expire in 24 hours. If you did not create an account on {{.Host}}, please disregard this message. + +Thank you for joining {{.Host}}! + +Best regards, + +{{.Footer}} diff --git a/internal/proto/users/v1/users.proto b/internal/proto/users/v1/users.proto new file mode 100644 index 0000000000..c4779ad55f --- /dev/null +++ b/internal/proto/users/v1/users.proto @@ -0,0 +1,27 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package users.v1; +option go_package = "github.com/absmach/supermq/api/grpc/users/v1"; + +// UsersService is a service that provides user-related functionalities +// for SuperMQ services. +service UsersService { + rpc SendEmail(SendEmailReq) returns (SendEmailRes) {} +} + +message SendEmailReq { + repeated string to = 1; // Email recipients + string from = 2; // Sender email address (optional) + string subject = 3; // Email subject + string header = 4; // Email header text + string user = 5; // User name + string content = 6; // Email content/URL + string footer = 7; // Email footer text +} + +message SendEmailRes { + bool sent = 1; // Whether the email was sent successfully +} diff --git a/pkg/grpcclient/client.go b/pkg/grpcclient/client.go index 7eb843617c..435de5a1de 100644 --- a/pkg/grpcclient/client.go +++ b/pkg/grpcclient/client.go @@ -11,11 +11,13 @@ import ( grpcDomainsV1 "github.com/absmach/supermq/api/grpc/domains/v1" grpcGroupsV1 "github.com/absmach/supermq/api/grpc/groups/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" + grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" tokengrpc "github.com/absmach/supermq/auth/api/grpc/token" channelsgrpc "github.com/absmach/supermq/channels/api/grpc" clientsauth "github.com/absmach/supermq/clients/api/grpc" domainsgrpc "github.com/absmach/supermq/domains/api/grpc" groupsgrpc "github.com/absmach/supermq/groups/api/grpc" + usersgrpc "github.com/absmach/supermq/users/api/grpc" grpchealth "google.golang.org/grpc/health/grpc_health_v1" ) @@ -97,3 +99,25 @@ func SetupGroupsClient(ctx context.Context, cfg Config) (grpcGroupsV1.GroupsServ return groupsgrpc.NewClient(client.Connection(), cfg.Timeout), client, nil } + +// SetupUsersClient loads users gRPC configuration and creates new users gRPC client. +// +// For example: +// +// usersClient, usersHandler, err := grpcclient.SetupUsersClient(ctx, grpcclient.Config{}). +func SetupUsersClient(ctx context.Context, cfg Config) (grpcUsersV1.UsersServiceClient, Handler, error) { + client, err := NewHandler(cfg) + if err != nil { + return nil, nil, err + } + + health := grpchealth.NewHealthClient(client.Connection()) + resp, err := health.Check(ctx, &grpchealth.HealthCheckRequest{ + Service: "users", + }) + if err != nil || resp.GetStatus() != grpchealth.HealthCheckResponse_SERVING { + return nil, nil, ErrSvcNotServing + } + + return usersgrpc.NewUsersClient(client.Connection(), cfg.Timeout), client, nil +} diff --git a/pkg/messaging/message.pb.go b/pkg/messaging/message.pb.go index 52bcd13b11..60b0a3c9d3 100644 --- a/pkg/messaging/message.pb.go +++ b/pkg/messaging/message.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc v5.29.0 // source: pkg/messaging/message.proto package messaging diff --git a/users/api/grpc/client.go b/users/api/grpc/client.go new file mode 100644 index 0000000000..ce656e31a1 --- /dev/null +++ b/users/api/grpc/client.go @@ -0,0 +1,92 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +import ( + "context" + "time" + + grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" + grpcapi "github.com/absmach/supermq/auth/api/grpc" + "github.com/go-kit/kit/endpoint" + kitgrpc "github.com/go-kit/kit/transport/grpc" + "google.golang.org/grpc" +) + +const usersSvcName = "users.v1.UsersService" + +var _ grpcUsersV1.UsersServiceClient = (*usersGrpcClient)(nil) + +type usersGrpcClient struct { + sendEmail endpoint.Endpoint + timeout time.Duration +} + +// NewUsersClient returns new users gRPC client instance. +func NewUsersClient(conn *grpc.ClientConn, timeout time.Duration) grpcUsersV1.UsersServiceClient { + return &usersGrpcClient{ + sendEmail: kitgrpc.NewClient( + conn, + usersSvcName, + "SendEmail", + encodeSendEmailClientRequest, + decodeSendEmailClientResponse, + grpcUsersV1.SendEmailRes{}, + ).Endpoint(), + timeout: timeout, + } +} + +func (client usersGrpcClient) SendEmail(ctx context.Context, in *grpcUsersV1.SendEmailReq, opts ...grpc.CallOption) (*grpcUsersV1.SendEmailRes, error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + res, err := client.sendEmail(ctx, sendEmailClientReq{ + to: in.GetTo(), + from: in.GetFrom(), + subject: in.GetSubject(), + header: in.GetHeader(), + user: in.GetUser(), + content: in.GetContent(), + footer: in.GetFooter(), + }) + if err != nil { + return &grpcUsersV1.SendEmailRes{}, grpcapi.DecodeError(err) + } + + ser := res.(sendEmailClientRes) + return &grpcUsersV1.SendEmailRes{Sent: ser.sent}, nil +} + +func decodeSendEmailClientResponse(_ context.Context, grpcRes any) (any, error) { + res := grpcRes.(*grpcUsersV1.SendEmailRes) + return sendEmailClientRes{sent: res.GetSent()}, nil +} + +func encodeSendEmailClientRequest(_ context.Context, grpcReq any) (any, error) { + req := grpcReq.(sendEmailClientReq) + return &grpcUsersV1.SendEmailReq{ + To: req.to, + From: req.from, + Subject: req.subject, + Header: req.header, + User: req.user, + Content: req.content, + Footer: req.footer, + }, nil +} + +type sendEmailClientReq struct { + to []string + from string + subject string + header string + user string + content string + footer string +} + +type sendEmailClientRes struct { + sent bool +} diff --git a/users/api/grpc/endpoint.go b/users/api/grpc/endpoint.go new file mode 100644 index 0000000000..91e53b3021 --- /dev/null +++ b/users/api/grpc/endpoint.go @@ -0,0 +1,51 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +import ( + "context" + + "github.com/absmach/supermq/pkg/errors" + "github.com/absmach/supermq/users" + "github.com/go-kit/kit/endpoint" +) + +func sendEmailEndpoint(svc users.Service) endpoint.Endpoint { + return func(ctx context.Context, request any) (any, error) { + req := request.(sendEmailReq) + if err := req.validate(); err != nil { + return sendEmailRes{}, err + } + + if err := svc.SendEmail(ctx, req.to, req.from, req.subject, req.header, req.user, req.content, req.footer); err != nil { + return sendEmailRes{}, err + } + + return sendEmailRes{sent: true}, nil + } +} + +type sendEmailReq struct { + to []string + from string + subject string + header string + user string + content string + footer string +} + +func (req sendEmailReq) validate() error { + if len(req.to) == 0 { + return errors.ErrMalformedEntity + } + if req.subject == "" { + return errors.ErrMalformedEntity + } + return nil +} + +type sendEmailRes struct { + sent bool +} diff --git a/users/api/grpc/server.go b/users/api/grpc/server.go new file mode 100644 index 0000000000..98315bdda6 --- /dev/null +++ b/users/api/grpc/server.go @@ -0,0 +1,57 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +import ( + "context" + + grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" + grpcapi "github.com/absmach/supermq/auth/api/grpc" + "github.com/absmach/supermq/users" + kitgrpc "github.com/go-kit/kit/transport/grpc" +) + +var _ grpcUsersV1.UsersServiceServer = (*usersGrpcServer)(nil) + +type usersGrpcServer struct { + grpcUsersV1.UnimplementedUsersServiceServer + sendEmail kitgrpc.Handler +} + +// NewUsersServer creates a new users gRPC server. +func NewUsersServer(svc users.Service) grpcUsersV1.UsersServiceServer { + return &usersGrpcServer{ + sendEmail: kitgrpc.NewServer( + sendEmailEndpoint(svc), + decodeSendEmailRequest, + encodeSendEmailResponse, + ), + } +} + +func decodeSendEmailRequest(_ context.Context, grpcReq any) (any, error) { + req := grpcReq.(*grpcUsersV1.SendEmailReq) + return sendEmailReq{ + to: req.GetTo(), + from: req.GetFrom(), + subject: req.GetSubject(), + header: req.GetHeader(), + user: req.GetUser(), + content: req.GetContent(), + footer: req.GetFooter(), + }, nil +} + +func encodeSendEmailResponse(_ context.Context, grpcRes any) (any, error) { + res := grpcRes.(sendEmailRes) + return &grpcUsersV1.SendEmailRes{Sent: res.sent}, nil +} + +func (s *usersGrpcServer) SendEmail(ctx context.Context, req *grpcUsersV1.SendEmailReq) (*grpcUsersV1.SendEmailRes, error) { + _, res, err := s.sendEmail.ServeGRPC(ctx, req) + if err != nil { + return nil, grpcapi.EncodeError(err) + } + return res.(*grpcUsersV1.SendEmailRes), nil +} diff --git a/users/emailer.go b/users/emailer.go index 4b93df933a..da946fdd57 100644 --- a/users/emailer.go +++ b/users/emailer.go @@ -10,4 +10,7 @@ type Emailer interface { // SendVerification sends an email to the user with a verification token. SendVerification(To []string, user, verificationToken string) error + + // Send sends an email with custom parameters. + Send(To []string, from, subject, header, user, content, footer string) error } diff --git a/users/emailer/emailer.go b/users/emailer/emailer.go index 6cf4bdfb30..3bf3ae7035 100644 --- a/users/emailer/emailer.go +++ b/users/emailer/emailer.go @@ -48,3 +48,8 @@ func (e *emailer) SendVerification(to []string, user, verificationToken string) url := fmt.Sprintf("%s?token=%s", e.verificationURL, verificationToken) return e.verifyAgent.Send(to, "", "Email Verification", "", user, url, "") } + +func (e *emailer) Send(to []string, from, subject, header, user, content, footer string) error { + // Use the reset agent as the default agent for custom emails + return e.resetAgent.Send(to, from, subject, header, user, content, footer) +} diff --git a/users/events/streams.go b/users/events/streams.go index ad3c5e376b..90c3779d31 100644 --- a/users/events/streams.go +++ b/users/events/streams.go @@ -444,3 +444,7 @@ func (es *eventStore) OAuthAddUserPolicy(ctx context.Context, user users.User) e return es.Publish(ctx, addPolicyStream, event) } + +func (es *eventStore) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { + return es.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) +} diff --git a/users/middleware/authorization.go b/users/middleware/authorization.go index 14b6a5d79f..5af68cc2b8 100644 --- a/users/middleware/authorization.go +++ b/users/middleware/authorization.go @@ -335,6 +335,10 @@ func (am *authorizationMiddleware) OAuthAddUserPolicy(ctx context.Context, user return am.svc.OAuthAddUserPolicy(ctx, user) } +func (am *authorizationMiddleware) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { + return am.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) +} + func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error { if session.Role != authn.AdminRole { return svcerr.ErrSuperAdminAction diff --git a/users/middleware/logging.go b/users/middleware/logging.go index 8339cfe9ca..90982fdd52 100644 --- a/users/middleware/logging.go +++ b/users/middleware/logging.go @@ -521,3 +521,22 @@ func (lm *loggingMiddleware) OAuthAddUserPolicy(ctx context.Context, user users. }(time.Now()) return lm.svc.OAuthAddUserPolicy(ctx, user) } + +// SendEmail logs the send_email request. It logs the recipients and the time it took to complete the request. +func (lm *loggingMiddleware) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("request_id", middleware.GetReqID(ctx)), + slog.Any("to", to), + slog.String("subject", subject), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Send email failed", args...) + return + } + lm.logger.Info("Send email completed successfully", args...) + }(time.Now()) + return lm.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) +} diff --git a/users/middleware/metrics.go b/users/middleware/metrics.go index 1095e267d0..a90a711ca5 100644 --- a/users/middleware/metrics.go +++ b/users/middleware/metrics.go @@ -245,3 +245,12 @@ func (ms *metricsMiddleware) OAuthAddUserPolicy(ctx context.Context, user users. }(time.Now()) return ms.svc.OAuthAddUserPolicy(ctx, user) } + +// SendEmail instruments SendEmail method with metrics. +func (ms *metricsMiddleware) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { + defer func(begin time.Time) { + ms.counter.With("method", "send_email").Add(1) + ms.latency.With("method", "send_email").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) +} diff --git a/users/middleware/tracing.go b/users/middleware/tracing.go index 9ee39763e7..a9303a4501 100644 --- a/users/middleware/tracing.go +++ b/users/middleware/tracing.go @@ -248,3 +248,14 @@ func (tm *tracingMiddleware) OAuthAddUserPolicy(ctx context.Context, user users. return tm.svc.OAuthAddUserPolicy(ctx, user) } + +// SendEmail traces the "SendEmail" operation of the wrapped users.Service. +func (tm *tracingMiddleware) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { + ctx, span := tracing.StartSpan(ctx, tm.tracer, "svc_send_email", trace.WithAttributes( + attribute.StringSlice("to", to), + attribute.String("subject", subject), + )) + defer span.End() + + return tm.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) +} diff --git a/users/mocks/emailer.go b/users/mocks/emailer.go index 5ebeea6845..387d88b460 100644 --- a/users/mocks/emailer.go +++ b/users/mocks/emailer.go @@ -39,6 +39,93 @@ func (_m *Emailer) EXPECT() *Emailer_Expecter { return &Emailer_Expecter{mock: &_m.Mock} } +// Send provides a mock function for the type Emailer +func (_mock *Emailer) Send(To []string, from string, subject string, header string, user string, content string, footer string) error { + ret := _mock.Called(To, from, subject, header, user, content, footer) + + if len(ret) == 0 { + panic("no return value specified for Send") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func([]string, string, string, string, string, string, string) error); ok { + r0 = returnFunc(To, from, subject, header, user, content, footer) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// Emailer_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' +type Emailer_Send_Call struct { + *mock.Call +} + +// Send is a helper method to define mock.On call +// - To []string +// - from string +// - subject string +// - header string +// - user string +// - content string +// - footer string +func (_e *Emailer_Expecter) Send(To interface{}, from interface{}, subject interface{}, header interface{}, user interface{}, content interface{}, footer interface{}) *Emailer_Send_Call { + return &Emailer_Send_Call{Call: _e.mock.On("Send", To, from, subject, header, user, content, footer)} +} + +func (_c *Emailer_Send_Call) Run(run func(To []string, from string, subject string, header string, user string, content string, footer string)) *Emailer_Send_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []string + if args[0] != nil { + arg0 = args[0].([]string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + var arg4 string + if args[4] != nil { + arg4 = args[4].(string) + } + var arg5 string + if args[5] != nil { + arg5 = args[5].(string) + } + var arg6 string + if args[6] != nil { + arg6 = args[6].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + arg5, + arg6, + ) + }) + return _c +} + +func (_c *Emailer_Send_Call) Return(err error) *Emailer_Send_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Emailer_Send_Call) RunAndReturn(run func(To []string, from string, subject string, header string, user string, content string, footer string) error) *Emailer_Send_Call { + _c.Call.Return(run) + return _c +} + // SendPasswordReset provides a mock function for the type Emailer func (_mock *Emailer) SendPasswordReset(To []string, user string, token string) error { ret := _mock.Called(To, user, token) diff --git a/users/mocks/service.go b/users/mocks/service.go index 74a090668b..a012c31316 100644 --- a/users/mocks/service.go +++ b/users/mocks/service.go @@ -867,6 +867,99 @@ func (_c *Service_SearchUsers_Call) RunAndReturn(run func(ctx context.Context, p return _c } +// SendEmail provides a mock function for the type Service +func (_mock *Service) SendEmail(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string) error { + ret := _mock.Called(ctx, to, from, subject, header, user, content, footer) + + if len(ret) == 0 { + panic("no return value specified for SendEmail") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, []string, string, string, string, string, string, string) error); ok { + r0 = returnFunc(ctx, to, from, subject, header, user, content, footer) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// Service_SendEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmail' +type Service_SendEmail_Call struct { + *mock.Call +} + +// SendEmail is a helper method to define mock.On call +// - ctx context.Context +// - to []string +// - from string +// - subject string +// - header string +// - user string +// - content string +// - footer string +func (_e *Service_Expecter) SendEmail(ctx interface{}, to interface{}, from interface{}, subject interface{}, header interface{}, user interface{}, content interface{}, footer interface{}) *Service_SendEmail_Call { + return &Service_SendEmail_Call{Call: _e.mock.On("SendEmail", ctx, to, from, subject, header, user, content, footer)} +} + +func (_c *Service_SendEmail_Call) Run(run func(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string)) *Service_SendEmail_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + if args[1] != nil { + arg1 = args[1].([]string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + var arg4 string + if args[4] != nil { + arg4 = args[4].(string) + } + var arg5 string + if args[5] != nil { + arg5 = args[5].(string) + } + var arg6 string + if args[6] != nil { + arg6 = args[6].(string) + } + var arg7 string + if args[7] != nil { + arg7 = args[7].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + arg5, + arg6, + arg7, + ) + }) + return _c +} + +func (_c *Service_SendEmail_Call) Return(err error) *Service_SendEmail_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Service_SendEmail_Call) RunAndReturn(run func(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string) error) *Service_SendEmail_Call { + _c.Call.Return(run) + return _c +} + // SendPasswordReset provides a mock function for the type Service func (_mock *Service) SendPasswordReset(ctx context.Context, email string) error { ret := _mock.Called(ctx, email) diff --git a/users/service.go b/users/service.go index a5cb975733..67dc2b79e3 100644 --- a/users/service.go +++ b/users/service.go @@ -814,3 +814,7 @@ func changed(updated *string, old string) bool { return *updated != old } + +func (svc service) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { + return svc.email.Send(to, from, subject, header, user, content, footer) +} diff --git a/users/users.go b/users/users.go index 4a269f4215..3381553656 100644 --- a/users/users.go +++ b/users/users.go @@ -243,4 +243,7 @@ type Service interface { // OAuthAddUserPolicy adds a policy to the user for an OAuth request. OAuthAddUserPolicy(ctx context.Context, user User) error + + // SendEmail sends an email using the email agent. + SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error } From 601a99c2695fa3f5e827f2ffbc97885a13dc3022 Mon Sep 17 00:00:00 2001 From: WashingtonKK Date: Fri, 31 Oct 2025 15:50:42 +0300 Subject: [PATCH 2/9] Send email on invitation Signed-off-by: WashingtonKK Fix tests Signed-off-by: WashingtonKK update protoc version Signed-off-by: WashingtonKK fix tests Signed-off-by: WashingtonKK lint proto Signed-off-by: WashingtonKK lint proto Signed-off-by: WashingtonKK --- api/grpc/auth/v1/auth.pb.go | 2 +- api/grpc/auth/v1/auth_grpc.pb.go | 2 +- api/grpc/channels/v1/channels.pb.go | 2 +- api/grpc/channels/v1/channels_grpc.pb.go | 2 +- api/grpc/clients/v1/clients.pb.go | 2 +- api/grpc/clients/v1/clients_grpc.pb.go | 2 +- api/grpc/common/v1/common.pb.go | 2 +- api/grpc/domains/v1/domains.pb.go | 2 +- api/grpc/domains/v1/domains_grpc.pb.go | 2 +- api/grpc/groups/v1/groups.pb.go | 2 +- api/grpc/groups/v1/groups_grpc.pb.go | 2 +- api/grpc/token/v1/token.pb.go | 2 +- api/grpc/token/v1/token_grpc.pb.go | 2 +- api/grpc/users/v1/users.pb.go | 56 +++++----- api/grpc/users/v1/users_grpc.pb.go | 30 +++--- channels/api/grpc/endpoint_test.go | 69 ++++++------ clients/api/grpc/endpoint_test.go | 86 +++++++-------- cmd/domains/main.go | 9 +- cmd/users/main.go | 14 ++- docker/.env | 1 + docker/docker-compose.yaml | 1 + domains/service.go | 20 +++- domains/service_test.go | 13 ++- groups/api/grpc/endpoint_test.go | 35 ++++--- internal/proto/users/v1/users.proto | 6 +- pkg/messaging/message.pb.go | 2 +- tools/config/.mockery.yaml | 5 + users/api/grpc/client.go | 8 +- users/api/grpc/endpoint.go | 2 +- users/api/grpc/server.go | 6 +- users/emailer.go | 3 + users/emailer/emailer.go | 14 ++- users/events/streams.go | 4 +- users/middleware/authorization.go | 4 +- users/middleware/logging.go | 6 +- users/middleware/metrics.go | 6 +- users/middleware/tracing.go | 6 +- users/mocks/emailer.go | 87 ++++++++++++++++ users/mocks/service.go | 22 ++-- users/mocks/users_service_client.go | 127 +++++++++++++++++++++++ users/service.go | 18 +++- users/users.go | 4 +- 42 files changed, 485 insertions(+), 205 deletions(-) create mode 100644 users/mocks/users_service_client.go diff --git a/api/grpc/auth/v1/auth.pb.go b/api/grpc/auth/v1/auth.pb.go index 18e2e08446..c71ceb3efc 100644 --- a/api/grpc/auth/v1/auth.pb.go +++ b/api/grpc/auth/v1/auth.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v5.29.0 +// protoc v6.33.0 // source: auth/v1/auth.proto package v1 diff --git a/api/grpc/auth/v1/auth_grpc.pb.go b/api/grpc/auth/v1/auth_grpc.pb.go index d85fabae7c..dbeeab1449 100644 --- a/api/grpc/auth/v1/auth_grpc.pb.go +++ b/api/grpc/auth/v1/auth_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.0 +// - protoc v6.33.0 // source: auth/v1/auth.proto package v1 diff --git a/api/grpc/channels/v1/channels.pb.go b/api/grpc/channels/v1/channels.pb.go index 22783fff04..94b4f230c3 100644 --- a/api/grpc/channels/v1/channels.pb.go +++ b/api/grpc/channels/v1/channels.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v5.29.0 +// protoc v6.33.0 // source: channels/v1/channels.proto package v1 diff --git a/api/grpc/channels/v1/channels_grpc.pb.go b/api/grpc/channels/v1/channels_grpc.pb.go index 9b9fe58856..aec78d4a4d 100644 --- a/api/grpc/channels/v1/channels_grpc.pb.go +++ b/api/grpc/channels/v1/channels_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.0 +// - protoc v6.33.0 // source: channels/v1/channels.proto package v1 diff --git a/api/grpc/clients/v1/clients.pb.go b/api/grpc/clients/v1/clients.pb.go index fd416e5f47..3fa2d5d73c 100644 --- a/api/grpc/clients/v1/clients.pb.go +++ b/api/grpc/clients/v1/clients.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v5.29.0 +// protoc v6.33.0 // source: clients/v1/clients.proto package v1 diff --git a/api/grpc/clients/v1/clients_grpc.pb.go b/api/grpc/clients/v1/clients_grpc.pb.go index bd04a8c47f..e6b66f08af 100644 --- a/api/grpc/clients/v1/clients_grpc.pb.go +++ b/api/grpc/clients/v1/clients_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.0 +// - protoc v6.33.0 // source: clients/v1/clients.proto package v1 diff --git a/api/grpc/common/v1/common.pb.go b/api/grpc/common/v1/common.pb.go index c1a24e5d73..18954ab2cc 100644 --- a/api/grpc/common/v1/common.pb.go +++ b/api/grpc/common/v1/common.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v5.29.0 +// protoc v6.33.0 // source: common/v1/common.proto package v1 diff --git a/api/grpc/domains/v1/domains.pb.go b/api/grpc/domains/v1/domains.pb.go index c51f9bd832..ac09207971 100644 --- a/api/grpc/domains/v1/domains.pb.go +++ b/api/grpc/domains/v1/domains.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v5.29.0 +// protoc v6.33.0 // source: domains/v1/domains.proto package v1 diff --git a/api/grpc/domains/v1/domains_grpc.pb.go b/api/grpc/domains/v1/domains_grpc.pb.go index 0171109a87..0fcf6e4d9a 100644 --- a/api/grpc/domains/v1/domains_grpc.pb.go +++ b/api/grpc/domains/v1/domains_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.0 +// - protoc v6.33.0 // source: domains/v1/domains.proto package v1 diff --git a/api/grpc/groups/v1/groups.pb.go b/api/grpc/groups/v1/groups.pb.go index 6b4fd65f0d..f62ab9e450 100644 --- a/api/grpc/groups/v1/groups.pb.go +++ b/api/grpc/groups/v1/groups.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v5.29.0 +// protoc v6.33.0 // source: groups/v1/groups.proto package v1 diff --git a/api/grpc/groups/v1/groups_grpc.pb.go b/api/grpc/groups/v1/groups_grpc.pb.go index d362f88c2b..e742a317cd 100644 --- a/api/grpc/groups/v1/groups_grpc.pb.go +++ b/api/grpc/groups/v1/groups_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.0 +// - protoc v6.33.0 // source: groups/v1/groups.proto package v1 diff --git a/api/grpc/token/v1/token.pb.go b/api/grpc/token/v1/token.pb.go index 3e75135363..937ee5fee4 100644 --- a/api/grpc/token/v1/token.pb.go +++ b/api/grpc/token/v1/token.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v5.29.0 +// protoc v6.33.0 // source: token/v1/token.proto package v1 diff --git a/api/grpc/token/v1/token_grpc.pb.go b/api/grpc/token/v1/token_grpc.pb.go index f3adacfb70..70ac6a7609 100644 --- a/api/grpc/token/v1/token_grpc.pb.go +++ b/api/grpc/token/v1/token_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.0 +// - protoc v6.33.0 // source: token/v1/token.proto package v1 diff --git a/api/grpc/users/v1/users.pb.go b/api/grpc/users/v1/users.pb.go index 879c699f95..be933374bb 100644 --- a/api/grpc/users/v1/users.pb.go +++ b/api/grpc/users/v1/users.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v5.29.0 +// protoc v6.33.0 // source: users/v1/users.proto package v1 @@ -24,9 +24,9 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type SendEmailReq struct { +type SendEmailWithUserIdReq struct { state protoimpl.MessageState `protogen:"open.v1"` - To []string `protobuf:"bytes,1,rep,name=to,proto3" json:"to,omitempty"` // Email recipients + Users []string `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` // Email recipients From string `protobuf:"bytes,2,opt,name=from,proto3" json:"from,omitempty"` // Sender email address (optional) Subject string `protobuf:"bytes,3,opt,name=subject,proto3" json:"subject,omitempty"` // Email subject Header string `protobuf:"bytes,4,opt,name=header,proto3" json:"header,omitempty"` // Email header text @@ -37,20 +37,20 @@ type SendEmailReq struct { sizeCache protoimpl.SizeCache } -func (x *SendEmailReq) Reset() { - *x = SendEmailReq{} +func (x *SendEmailWithUserIdReq) Reset() { + *x = SendEmailWithUserIdReq{} mi := &file_users_v1_users_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SendEmailReq) String() string { +func (x *SendEmailWithUserIdReq) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SendEmailReq) ProtoMessage() {} +func (*SendEmailWithUserIdReq) ProtoMessage() {} -func (x *SendEmailReq) ProtoReflect() protoreflect.Message { +func (x *SendEmailWithUserIdReq) ProtoReflect() protoreflect.Message { mi := &file_users_v1_users_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -62,54 +62,54 @@ func (x *SendEmailReq) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SendEmailReq.ProtoReflect.Descriptor instead. -func (*SendEmailReq) Descriptor() ([]byte, []int) { +// Deprecated: Use SendEmailWithUserIdReq.ProtoReflect.Descriptor instead. +func (*SendEmailWithUserIdReq) Descriptor() ([]byte, []int) { return file_users_v1_users_proto_rawDescGZIP(), []int{0} } -func (x *SendEmailReq) GetTo() []string { +func (x *SendEmailWithUserIdReq) GetUsers() []string { if x != nil { - return x.To + return x.Users } return nil } -func (x *SendEmailReq) GetFrom() string { +func (x *SendEmailWithUserIdReq) GetFrom() string { if x != nil { return x.From } return "" } -func (x *SendEmailReq) GetSubject() string { +func (x *SendEmailWithUserIdReq) GetSubject() string { if x != nil { return x.Subject } return "" } -func (x *SendEmailReq) GetHeader() string { +func (x *SendEmailWithUserIdReq) GetHeader() string { if x != nil { return x.Header } return "" } -func (x *SendEmailReq) GetUser() string { +func (x *SendEmailWithUserIdReq) GetUser() string { if x != nil { return x.User } return "" } -func (x *SendEmailReq) GetContent() string { +func (x *SendEmailWithUserIdReq) GetContent() string { if x != nil { return x.Content } return "" } -func (x *SendEmailReq) GetFooter() string { +func (x *SendEmailWithUserIdReq) GetFooter() string { if x != nil { return x.Footer } @@ -164,9 +164,9 @@ var File_users_v1_users_proto protoreflect.FileDescriptor const file_users_v1_users_proto_rawDesc = "" + "\n" + - "\x14users/v1/users.proto\x12\busers.v1\"\xaa\x01\n" + - "\fSendEmailReq\x12\x0e\n" + - "\x02to\x18\x01 \x03(\tR\x02to\x12\x12\n" + + "\x14users/v1/users.proto\x12\busers.v1\"\xba\x01\n" + + "\x16SendEmailWithUserIdReq\x12\x14\n" + + "\x05users\x18\x01 \x03(\tR\x05users\x12\x12\n" + "\x04from\x18\x02 \x01(\tR\x04from\x12\x18\n" + "\asubject\x18\x03 \x01(\tR\asubject\x12\x16\n" + "\x06header\x18\x04 \x01(\tR\x06header\x12\x12\n" + @@ -174,9 +174,9 @@ const file_users_v1_users_proto_rawDesc = "" + "\acontent\x18\x06 \x01(\tR\acontent\x12\x16\n" + "\x06footer\x18\a \x01(\tR\x06footer\"\"\n" + "\fSendEmailRes\x12\x12\n" + - "\x04sent\x18\x01 \x01(\bR\x04sent2M\n" + - "\fUsersService\x12=\n" + - "\tSendEmail\x12\x16.users.v1.SendEmailReq\x1a\x16.users.v1.SendEmailRes\"\x00B.Z,github.com/absmach/supermq/api/grpc/users/v1b\x06proto3" + "\x04sent\x18\x01 \x01(\bR\x04sent2a\n" + + "\fUsersService\x12Q\n" + + "\x13SendEmailWithUserId\x12 .users.v1.SendEmailWithUserIdReq\x1a\x16.users.v1.SendEmailRes\"\x00B.Z,github.com/absmach/supermq/api/grpc/users/v1b\x06proto3" var ( file_users_v1_users_proto_rawDescOnce sync.Once @@ -192,12 +192,12 @@ func file_users_v1_users_proto_rawDescGZIP() []byte { var file_users_v1_users_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_users_v1_users_proto_goTypes = []any{ - (*SendEmailReq)(nil), // 0: users.v1.SendEmailReq - (*SendEmailRes)(nil), // 1: users.v1.SendEmailRes + (*SendEmailWithUserIdReq)(nil), // 0: users.v1.SendEmailWithUserIdReq + (*SendEmailRes)(nil), // 1: users.v1.SendEmailRes } var file_users_v1_users_proto_depIdxs = []int32{ - 0, // 0: users.v1.UsersService.SendEmail:input_type -> users.v1.SendEmailReq - 1, // 1: users.v1.UsersService.SendEmail:output_type -> users.v1.SendEmailRes + 0, // 0: users.v1.UsersService.SendEmailWithUserId:input_type -> users.v1.SendEmailWithUserIdReq + 1, // 1: users.v1.UsersService.SendEmailWithUserId:output_type -> users.v1.SendEmailRes 1, // [1:2] is the sub-list for method output_type 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name diff --git a/api/grpc/users/v1/users_grpc.pb.go b/api/grpc/users/v1/users_grpc.pb.go index 7417e78553..8b6b0bd265 100644 --- a/api/grpc/users/v1/users_grpc.pb.go +++ b/api/grpc/users/v1/users_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.0 +// - protoc v6.33.0 // source: users/v1/users.proto package v1 @@ -22,7 +22,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - UsersService_SendEmail_FullMethodName = "/users.v1.UsersService/SendEmail" + UsersService_SendEmailWithUserId_FullMethodName = "/users.v1.UsersService/SendEmailWithUserId" ) // UsersServiceClient is the client API for UsersService service. @@ -32,7 +32,7 @@ const ( // UsersService is a service that provides user-related functionalities // for SuperMQ services. type UsersServiceClient interface { - SendEmail(ctx context.Context, in *SendEmailReq, opts ...grpc.CallOption) (*SendEmailRes, error) + SendEmailWithUserId(ctx context.Context, in *SendEmailWithUserIdReq, opts ...grpc.CallOption) (*SendEmailRes, error) } type usersServiceClient struct { @@ -43,10 +43,10 @@ func NewUsersServiceClient(cc grpc.ClientConnInterface) UsersServiceClient { return &usersServiceClient{cc} } -func (c *usersServiceClient) SendEmail(ctx context.Context, in *SendEmailReq, opts ...grpc.CallOption) (*SendEmailRes, error) { +func (c *usersServiceClient) SendEmailWithUserId(ctx context.Context, in *SendEmailWithUserIdReq, opts ...grpc.CallOption) (*SendEmailRes, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SendEmailRes) - err := c.cc.Invoke(ctx, UsersService_SendEmail_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, UsersService_SendEmailWithUserId_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -60,7 +60,7 @@ func (c *usersServiceClient) SendEmail(ctx context.Context, in *SendEmailReq, op // UsersService is a service that provides user-related functionalities // for SuperMQ services. type UsersServiceServer interface { - SendEmail(context.Context, *SendEmailReq) (*SendEmailRes, error) + SendEmailWithUserId(context.Context, *SendEmailWithUserIdReq) (*SendEmailRes, error) mustEmbedUnimplementedUsersServiceServer() } @@ -71,8 +71,8 @@ type UsersServiceServer interface { // pointer dereference when methods are called. type UnimplementedUsersServiceServer struct{} -func (UnimplementedUsersServiceServer) SendEmail(context.Context, *SendEmailReq) (*SendEmailRes, error) { - return nil, status.Errorf(codes.Unimplemented, "method SendEmail not implemented") +func (UnimplementedUsersServiceServer) SendEmailWithUserId(context.Context, *SendEmailWithUserIdReq) (*SendEmailRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendEmailWithUserId not implemented") } func (UnimplementedUsersServiceServer) mustEmbedUnimplementedUsersServiceServer() {} func (UnimplementedUsersServiceServer) testEmbeddedByValue() {} @@ -95,20 +95,20 @@ func RegisterUsersServiceServer(s grpc.ServiceRegistrar, srv UsersServiceServer) s.RegisterService(&UsersService_ServiceDesc, srv) } -func _UsersService_SendEmail_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SendEmailReq) +func _UsersService_SendEmailWithUserId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendEmailWithUserIdReq) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(UsersServiceServer).SendEmail(ctx, in) + return srv.(UsersServiceServer).SendEmailWithUserId(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: UsersService_SendEmail_FullMethodName, + FullMethod: UsersService_SendEmailWithUserId_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(UsersServiceServer).SendEmail(ctx, req.(*SendEmailReq)) + return srv.(UsersServiceServer).SendEmailWithUserId(ctx, req.(*SendEmailWithUserIdReq)) } return interceptor(ctx, in, info, handler) } @@ -121,8 +121,8 @@ var UsersService_ServiceDesc = grpc.ServiceDesc{ HandlerType: (*UsersServiceServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "SendEmail", - Handler: _UsersService_SendEmail_Handler, + MethodName: "SendEmailWithUserId", + Handler: _UsersService_SendEmailWithUserId_Handler, }, }, Streams: []grpc.StreamDesc{}, diff --git a/channels/api/grpc/endpoint_test.go b/channels/api/grpc/endpoint_test.go index fce4df2f36..4014474d39 100644 --- a/channels/api/grpc/endpoint_test.go +++ b/channels/api/grpc/endpoint_test.go @@ -29,7 +29,7 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -const port = 7005 +const port = 0 var ( validID = testsutil.GenerateUUID(&testing.T{}) @@ -40,26 +40,27 @@ var ( } ) -func startGRPCServer(svc *mocks.Service, port int) *grpc.Server { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) - if err != nil { - panic(fmt.Sprintf("failed to obtain port: %s", err)) - } - server := grpc.NewServer() - grpcChannelsV1.RegisterChannelsServiceServer(server, grpcapi.NewServer(svc)) - go func() { - if err := server.Serve(listener); err != nil { - panic(fmt.Sprintf("failed to serve: %s", err)) - } - }() - return server +func startGRPCServer(svc *mocks.Service) (*grpc.Server, int) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(fmt.Sprintf("failed to obtain port: %s", err)) + } + server := grpc.NewServer() + grpcChannelsV1.RegisterChannelsServiceServer(server, grpcapi.NewServer(svc)) + go func() { + if err := server.Serve(listener); err != nil { + panic(fmt.Sprintf("failed to serve: %s", err)) + } + }() + p := listener.Addr().(*net.TCPAddr).Port + return server, p } func TestAuthorize(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -134,10 +135,10 @@ func TestAuthorize(t *testing.T) { } func TestRemoveClientConnections(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -173,10 +174,10 @@ func TestRemoveClientConnections(t *testing.T) { } func TestUnsetParentGroupFromChannelsEndpoint(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -217,10 +218,10 @@ func TestUnsetParentGroupFromChannelsEndpoint(t *testing.T) { } func TestRetrieveEntity(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -268,10 +269,10 @@ func TestRetrieveEntity(t *testing.T) { } func TestRetrieveIDByRoute(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) diff --git a/clients/api/grpc/endpoint_test.go b/clients/api/grpc/endpoint_test.go index c1a6a05177..2e18fb9372 100644 --- a/clients/api/grpc/endpoint_test.go +++ b/clients/api/grpc/endpoint_test.go @@ -25,7 +25,7 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -const port = 7006 +const port = 0 var ( validID = testsutil.GenerateUUID(&testing.T{}) @@ -38,27 +38,27 @@ var ( } ) -func startGRPCServer(svc *mocks.Service, port int) *grpc.Server { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) - if err != nil { - panic(fmt.Sprintf("failed to obtain port: %s", err)) - } - server := grpc.NewServer() - grpcClientsV1.RegisterClientsServiceServer(server, grpcapi.NewServer(svc)) - go func() { - if err := server.Serve(listener); err != nil { - panic(fmt.Sprintf("failed to serve: %s", err)) - } - }() - - return server +func startGRPCServer(svc *mocks.Service) (*grpc.Server, int) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(fmt.Sprintf("failed to obtain port: %s", err)) + } + server := grpc.NewServer() + grpcClientsV1.RegisterClientsServiceServer(server, grpcapi.NewServer(svc)) + go func() { + if err := server.Serve(listener); err != nil { + panic(fmt.Sprintf("failed to serve: %s", err)) + } + }() + p := listener.Addr().(*net.TCPAddr).Port + return server, p } func TestAuthenticate(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -106,10 +106,10 @@ func TestAuthenticate(t *testing.T) { } func TestRetrieveEntity(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -162,10 +162,10 @@ func TestRetrieveEntity(t *testing.T) { } func TestRetrieveEntities(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -229,10 +229,10 @@ func TestRetrieveEntities(t *testing.T) { } func TestAddConnections(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -284,10 +284,10 @@ func TestAddConnections(t *testing.T) { } func TestRemoveConnections(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -339,10 +339,10 @@ func TestRemoveConnections(t *testing.T) { } func TestRemoveChannelConnections(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -380,10 +380,10 @@ func TestRemoveChannelConnections(t *testing.T) { } func TestUnsetParentGroupFromClient(t *testing.T) { - svc := new(mocks.Service) - server := startGRPCServer(svc, port) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", port) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) diff --git a/cmd/domains/main.go b/cmd/domains/main.go index 040f8b3cca..43548b68f4 100644 --- a/cmd/domains/main.go +++ b/cmd/domains/main.go @@ -15,6 +15,7 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/supermq" grpcDomainsV1 "github.com/absmach/supermq/api/grpc/domains/v1" + grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" "github.com/absmach/supermq/domains" domainsSvc "github.com/absmach/supermq/domains" domainsgrpcapi "github.com/absmach/supermq/domains/api/grpc" @@ -169,7 +170,7 @@ func main() { return } - _, usersHandler, err := grpcclient.SetupUsersClient(ctx, usersClientConfig) + usersClient, usersHandler, err := grpcclient.SetupUsersClient(ctx, usersClientConfig) if err != nil { logger.Error(fmt.Sprintf("failed to connect to users gRPC server : %s", err.Error())) exitCode = 1 @@ -225,7 +226,7 @@ func main() { return } - svc, err := newDomainService(ctx, domainsRepo, cache, tracer, cfg, authz, policyService, logger, call) + svc, err := newDomainService(ctx, domainsRepo, cache, tracer, cfg, authz, policyService, logger, call, usersClient) if err != nil { logger.Error(fmt.Sprintf("failed to create %s service: %s", svcName, err.Error())) exitCode = 1 @@ -277,7 +278,7 @@ func main() { } } -func newDomainService(ctx context.Context, domainsRepo domainsSvc.Repository, cache domainsSvc.Cache, tracer trace.Tracer, cfg config, authz authz.Authorization, policiessvc policies.Service, logger *slog.Logger, callout callout.Callout) (domains.Service, error) { +func newDomainService(ctx context.Context, domainsRepo domainsSvc.Repository, cache domainsSvc.Cache, tracer trace.Tracer, cfg config, authz authz.Authorization, policiessvc policies.Service, logger *slog.Logger, callout callout.Callout, usersClient grpcUsersV1.UsersServiceClient) (domains.Service, error) { idProvider := uuid.New() sidProvider, err := sid.New() if err != nil { @@ -289,7 +290,7 @@ func newDomainService(ctx context.Context, domainsRepo domainsSvc.Repository, ca return nil, err } - svc, err := domainsSvc.New(domainsRepo, cache, policiessvc, idProvider, sidProvider, availableActions, builtInRoles) + svc, err := domainsSvc.New(domainsRepo, cache, policiessvc, idProvider, sidProvider, availableActions, builtInRoles, usersClient) if err != nil { return nil, fmt.Errorf("failed to init domain service: %w", err) } diff --git a/cmd/users/main.go b/cmd/users/main.go index c99b50f565..68d17cede7 100644 --- a/cmd/users/main.go +++ b/cmd/users/main.go @@ -95,6 +95,7 @@ type config struct { SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"` PasswordResetURLPrefix string `env:"SMQ_PASSWORD_RESET_URL_PREFIX" envDefault:"http://localhost/password/reset"` PasswordResetEmailTemplate string `env:"SMQ_PASSWORD_RESET_EMAIL_TEMPLATE" envDefault:"reset-password-email.tmpl"` + EmailTemplate string `env:"SM_EMAIL_TEMPLATE" envDefault:"email.tmpl"` VerificationURLPrefix string `env:"SMQ_VERIFICATION_URL_PREFIX" envDefault:"http://localhost/verify-email"` VerificationEmailTemplate string `env:"SMQ_VERIFICATION_EMAIL_TEMPLATE" envDefault:"verification-email.tmpl"` PassRegex *regexp.Regexp @@ -146,6 +147,14 @@ func main() { } verificationEmailConfig.Template = cfg.VerificationEmailTemplate + customEmailConfig := email.Config{} + if err := env.Parse(&customEmailConfig); err != nil { + logger.Error(fmt.Sprintf("failed to load custom email configuration : %s", err.Error())) + exitCode = 1 + return + } + customEmailConfig.Template = cfg.EmailTemplate + dbConfig := pgclient.Config{Name: defDB} if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil { logger.Error(err.Error()) @@ -233,7 +242,7 @@ func main() { } logger.Info("Policy client successfully connected to spicedb gRPC server") - csvc, err := newService(ctx, authz, tokenClient, policyService, domainsClient, db, dbConfig, tracer, cfg, resetPasswordEmailConfig, verificationEmailConfig, logger) + csvc, err := newService(ctx, authz, tokenClient, policyService, domainsClient, db, dbConfig, tracer, cfg, resetPasswordEmailConfig, verificationEmailConfig, customEmailConfig, logger) if err != nil { logger.Error(fmt.Sprintf("failed to setup service: %s", err)) exitCode = 1 @@ -294,7 +303,7 @@ func main() { } } -func newService(ctx context.Context, authz smqauthz.Authorization, token grpcTokenV1.TokenServiceClient, policyService policies.Service, domainsClient grpcDomainsV1.DomainsServiceClient, db *sqlx.DB, dbConfig pgclient.Config, tracer trace.Tracer, c config, resetPasswordEmailConfig, verificationEmailConfig email.Config, logger *slog.Logger) (users.Service, error) { +func newService(ctx context.Context, authz smqauthz.Authorization, token grpcTokenV1.TokenServiceClient, policyService policies.Service, domainsClient grpcDomainsV1.DomainsServiceClient, db *sqlx.DB, dbConfig pgclient.Config, tracer trace.Tracer, c config, resetPasswordEmailConfig, verificationEmailConfig, customEmailConfig email.Config, logger *slog.Logger) (users.Service, error) { database := pg.NewDatabase(db, dbConfig, tracer) idp := uuid.New() hsr := hasher.New() @@ -307,6 +316,7 @@ func newService(ctx context.Context, authz smqauthz.Authorization, token grpcTok c.VerificationURLPrefix, &resetPasswordEmailConfig, &verificationEmailConfig, + &customEmailConfig, ) if err != nil { logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error())) diff --git a/docker/.env b/docker/.env index 8113d422ef..7e3f85076c 100644 --- a/docker/.env +++ b/docker/.env @@ -277,6 +277,7 @@ SMQ_PASSWORD_RESET_URL_PREFIX=http://localhost/password-reset SMQ_PASSWORD_RESET_EMAIL_TEMPLATE=reset-password-email.tmpl SMQ_VERIFICATION_URL_PREFIX=http://localhost/verify-email SMQ_VERIFICATION_EMAIL_TEMPLATE=verification-email.tmpl +SM_EMAIL_TEMPLATE=invitation.tmpl #### Users Client Config SMQ_USERS_URL=users:9002 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index fda0a53af6..745a35c56f 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -912,6 +912,7 @@ services: volumes: - ./templates/${SMQ_PASSWORD_RESET_EMAIL_TEMPLATE}:/${SMQ_PASSWORD_RESET_EMAIL_TEMPLATE} - ./templates/${SMQ_VERIFICATION_EMAIL_TEMPLATE}:/${SMQ_VERIFICATION_EMAIL_TEMPLATE} + - ./templates/${SMQ_EMAIL_TEMPLATE}:/${SMQ_EMAIL_TEMPLATE} # Auth gRPC client certificates - type: bind source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} diff --git a/domains/service.go b/domains/service.go index 7614665ed5..0c7fb760e4 100644 --- a/domains/service.go +++ b/domains/service.go @@ -8,6 +8,7 @@ import ( "time" "github.com/absmach/supermq" + grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/pkg/errors" repoerr "github.com/absmach/supermq/pkg/errors/repository" @@ -22,16 +23,17 @@ var ( ) type service struct { - repo Repository - cache Cache - policy policies.Service - idProvider supermq.IDProvider + repo Repository + cache Cache + policy policies.Service + idProvider supermq.IDProvider + usersClient grpcUsersV1.UsersServiceClient roles.ProvisionManageService } var _ Service = (*service)(nil) -func New(repo Repository, cache Cache, policy policies.Service, idProvider supermq.IDProvider, sidProvider supermq.IDProvider, availableActions []roles.Action, builtInRoles map[roles.BuiltInRoleName][]roles.Action) (Service, error) { +func New(repo Repository, cache Cache, policy policies.Service, idProvider supermq.IDProvider, sidProvider supermq.IDProvider, availableActions []roles.Action, builtInRoles map[roles.BuiltInRoleName][]roles.Action, usersClient grpcUsersV1.UsersServiceClient) (Service, error) { rpms, err := roles.NewProvisionManageService(policies.DomainType, repo, policy, sidProvider, availableActions, builtInRoles) if err != nil { return nil, err @@ -42,6 +44,7 @@ func New(repo Repository, cache Cache, policy policies.Service, idProvider super cache: cache, policy: policy, idProvider: idProvider, + usersClient: usersClient, ProvisionManageService: rpms, }, nil } @@ -205,6 +208,13 @@ func (svc *service) SendInvitation(ctx context.Context, session authn.Session, i if err := svc.repo.SaveInvitation(ctx, invitation); err != nil { return errors.Wrap(svcerr.ErrCreateEntity, err) } + + svc.usersClient.SendEmailWithUserId(ctx, &grpcUsersV1.SendEmailWithUserIdReq{ + Users: []string{invitation.InviteeUserID}, + Subject: "Invitation to join Domain", + From: invitation.InvitedBy, + }) + return nil } diff --git a/domains/service_test.go b/domains/service_test.go index 5025030c48..b31f742e56 100644 --- a/domains/service_test.go +++ b/domains/service_test.go @@ -21,6 +21,7 @@ import ( "github.com/absmach/supermq/pkg/roles" "github.com/absmach/supermq/pkg/sid" "github.com/absmach/supermq/pkg/uuid" + uMocks "github.com/absmach/supermq/users/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -82,9 +83,10 @@ var ( ) var ( - drepo *mocks.Repository - dcache *mocks.Cache - policy *policiesMocks.Service + drepo *mocks.Repository + dcache *mocks.Cache + policy *policiesMocks.Service + usersClient *uMocks.UsersServiceClient ) func newService() domains.Service { @@ -93,11 +95,12 @@ func newService() domains.Service { idProvider := uuid.NewMock() sidProvider := sid.NewMock() policy = new(policiesMocks.Service) + usersClient = &uMocks.UsersServiceClient{} availableActions := []roles.Action{} builtInRoles := map[roles.BuiltInRoleName][]roles.Action{ groups.BuiltInRoleAdmin: availableActions, } - ds, _ := domains.New(drepo, dcache, policy, idProvider, sidProvider, availableActions, builtInRoles) + ds, _ := domains.New(drepo, dcache, policy, idProvider, sidProvider, availableActions, builtInRoles, usersClient) return ds } @@ -699,12 +702,14 @@ func TestSendInvitation(t *testing.T) { repoCall1 := drepo.On("SaveInvitation", context.Background(), mock.Anything).Return(tc.createInvitationErr) repoCall2 := drepo.On("RetrieveInvitation", context.Background(), tc.req.InviteeUserID, tc.req.DomainID).Return(tc.retrieveInvRes, tc.retrieveInvErr) repoCall3 := drepo.On("UpdateRejection", context.Background(), mock.Anything).Return(tc.updateRejectionErr) + usersClientCall := usersClient.On("SendEmailWithUserId", mock.Anything, mock.Anything).Return(nil, nil) err := svc.SendInvitation(context.Background(), tc.session, tc.req) assert.True(t, errors.Contains(err, tc.err)) repoCall.Unset() repoCall1.Unset() repoCall2.Unset() repoCall3.Unset() + usersClientCall.Unset() }) } } diff --git a/groups/api/grpc/endpoint_test.go b/groups/api/grpc/endpoint_test.go index 12aae12247..7c2358a689 100644 --- a/groups/api/grpc/endpoint_test.go +++ b/groups/api/grpc/endpoint_test.go @@ -25,7 +25,7 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -const port = 7004 +const port = 0 var ( validID = testsutil.GenerateUUID(&testing.T{}) @@ -48,24 +48,27 @@ var ( } ) -func startGRPCServer(svc *prmocks.Service, port int) { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) - if err != nil { - panic(fmt.Sprintf("failed to obtain port: %s", err)) - } - server := grpc.NewServer() - grpcGroupsV1.RegisterGroupsServiceServer(server, grpcapi.NewServer(svc)) - go func() { - if err := server.Serve(listener); err != nil { - panic(fmt.Sprintf("failed to serve: %s", err)) - } - }() +func startGRPCServer(svc *prmocks.Service) (*grpc.Server, int) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(fmt.Sprintf("failed to obtain port: %s", err)) + } + server := grpc.NewServer() + grpcGroupsV1.RegisterGroupsServiceServer(server, grpcapi.NewServer(svc)) + go func() { + if err := server.Serve(listener); err != nil { + panic(fmt.Sprintf("failed to serve: %s", err)) + } + }() + p := listener.Addr().(*net.TCPAddr).Port + return server, p } func TestRetrieveEntityEndpoint(t *testing.T) { - svc := new(prmocks.Service) - startGRPCServer(svc, port) - grpAddr := fmt.Sprintf("localhost:%d", port) + svc := new(prmocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + grpAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(grpAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) diff --git a/internal/proto/users/v1/users.proto b/internal/proto/users/v1/users.proto index c4779ad55f..b1ace05e00 100644 --- a/internal/proto/users/v1/users.proto +++ b/internal/proto/users/v1/users.proto @@ -9,11 +9,11 @@ option go_package = "github.com/absmach/supermq/api/grpc/users/v1"; // UsersService is a service that provides user-related functionalities // for SuperMQ services. service UsersService { - rpc SendEmail(SendEmailReq) returns (SendEmailRes) {} + rpc SendEmailWithUserId(SendEmailWithUserIdReq) returns (SendEmailRes) {} } -message SendEmailReq { - repeated string to = 1; // Email recipients +message SendEmailWithUserIdReq { + repeated string users = 1; // Email recipients string from = 2; // Sender email address (optional) string subject = 3; // Email subject string header = 4; // Email header text diff --git a/pkg/messaging/message.pb.go b/pkg/messaging/message.pb.go index 60b0a3c9d3..52bcd13b11 100644 --- a/pkg/messaging/message.pb.go +++ b/pkg/messaging/message.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.10 -// protoc v5.29.0 +// protoc v6.33.0 // source: pkg/messaging/message.proto package messaging diff --git a/tools/config/.mockery.yaml b/tools/config/.mockery.yaml index 6e8c57e0aa..8315a80367 100644 --- a/tools/config/.mockery.yaml +++ b/tools/config/.mockery.yaml @@ -52,6 +52,11 @@ packages: dir: "./pkg/sdk/mocks" structname: "SDK" filename: "sdk.go" + github.com/absmach/supermq/api/grpc/users/v1: + interfaces: + UsersServiceClient: + config: + dir: "./users/mocks" github.com/absmach/supermq/auth: interfaces: Authz: diff --git a/users/api/grpc/client.go b/users/api/grpc/client.go index ce656e31a1..772b68ed36 100644 --- a/users/api/grpc/client.go +++ b/users/api/grpc/client.go @@ -38,12 +38,12 @@ func NewUsersClient(conn *grpc.ClientConn, timeout time.Duration) grpcUsersV1.Us } } -func (client usersGrpcClient) SendEmail(ctx context.Context, in *grpcUsersV1.SendEmailReq, opts ...grpc.CallOption) (*grpcUsersV1.SendEmailRes, error) { +func (client usersGrpcClient) SendEmailWithUserId(ctx context.Context, in *grpcUsersV1.SendEmailWithUserIdReq, opts ...grpc.CallOption) (*grpcUsersV1.SendEmailRes, error) { ctx, cancel := context.WithTimeout(ctx, client.timeout) defer cancel() res, err := client.sendEmail(ctx, sendEmailClientReq{ - to: in.GetTo(), + to: in.GetUsers(), from: in.GetFrom(), subject: in.GetSubject(), header: in.GetHeader(), @@ -66,8 +66,8 @@ func decodeSendEmailClientResponse(_ context.Context, grpcRes any) (any, error) func encodeSendEmailClientRequest(_ context.Context, grpcReq any) (any, error) { req := grpcReq.(sendEmailClientReq) - return &grpcUsersV1.SendEmailReq{ - To: req.to, + return &grpcUsersV1.SendEmailWithUserIdReq{ + Users: req.to, From: req.from, Subject: req.subject, Header: req.header, diff --git a/users/api/grpc/endpoint.go b/users/api/grpc/endpoint.go index 91e53b3021..3d292ce1ff 100644 --- a/users/api/grpc/endpoint.go +++ b/users/api/grpc/endpoint.go @@ -18,7 +18,7 @@ func sendEmailEndpoint(svc users.Service) endpoint.Endpoint { return sendEmailRes{}, err } - if err := svc.SendEmail(ctx, req.to, req.from, req.subject, req.header, req.user, req.content, req.footer); err != nil { + if err := svc.SendEmailWithUserId(ctx, req.to, req.from, req.subject, req.header, req.user, req.content, req.footer); err != nil { return sendEmailRes{}, err } diff --git a/users/api/grpc/server.go b/users/api/grpc/server.go index 98315bdda6..dd3ec0c72d 100644 --- a/users/api/grpc/server.go +++ b/users/api/grpc/server.go @@ -31,9 +31,9 @@ func NewUsersServer(svc users.Service) grpcUsersV1.UsersServiceServer { } func decodeSendEmailRequest(_ context.Context, grpcReq any) (any, error) { - req := grpcReq.(*grpcUsersV1.SendEmailReq) + req := grpcReq.(*grpcUsersV1.SendEmailWithUserIdReq) return sendEmailReq{ - to: req.GetTo(), + to: req.GetUsers(), from: req.GetFrom(), subject: req.GetSubject(), header: req.GetHeader(), @@ -48,7 +48,7 @@ func encodeSendEmailResponse(_ context.Context, grpcRes any) (any, error) { return &grpcUsersV1.SendEmailRes{Sent: res.sent}, nil } -func (s *usersGrpcServer) SendEmail(ctx context.Context, req *grpcUsersV1.SendEmailReq) (*grpcUsersV1.SendEmailRes, error) { +func (s *usersGrpcServer) SendEmail(ctx context.Context, req *grpcUsersV1.SendEmailWithUserIdReq) (*grpcUsersV1.SendEmailRes, error) { _, res, err := s.sendEmail.ServeGRPC(ctx, req) if err != nil { return nil, grpcapi.EncodeError(err) diff --git a/users/emailer.go b/users/emailer.go index da946fdd57..7859236c3b 100644 --- a/users/emailer.go +++ b/users/emailer.go @@ -13,4 +13,7 @@ type Emailer interface { // Send sends an email with custom parameters. Send(To []string, from, subject, header, user, content, footer string) error + + // SendCustom sends an email with custom parameters using a custom email agent. + SendCustom(To []string, from, subject, header, user, content, footer string) error } diff --git a/users/emailer/emailer.go b/users/emailer/emailer.go index 3bf3ae7035..4eef2f2df2 100644 --- a/users/emailer/emailer.go +++ b/users/emailer/emailer.go @@ -17,10 +17,11 @@ type emailer struct { verificationURL string resetAgent *email.Agent verifyAgent *email.Agent + customAgent *email.Agent } // New creates new emailer utility. -func New(resetURL, verificationURL string, resetConfig, verifyConfig *email.Config) (users.Emailer, error) { +func New(resetURL, verificationURL string, resetConfig, verifyConfig, customEmailConfig *email.Config) (users.Emailer, error) { resetAgent, err := email.New(resetConfig) if err != nil { return nil, err @@ -31,11 +32,17 @@ func New(resetURL, verificationURL string, resetConfig, verifyConfig *email.Conf return nil, err } + customAgent, err := email.New(customEmailConfig) + if err != nil { + return nil, err + } + return &emailer{ resetURL: resetURL, verificationURL: verificationURL, resetAgent: resetAgent, verifyAgent: verifyAgent, + customAgent: customAgent, }, nil } @@ -53,3 +60,8 @@ func (e *emailer) Send(to []string, from, subject, header, user, content, footer // Use the reset agent as the default agent for custom emails return e.resetAgent.Send(to, from, subject, header, user, content, footer) } + +func (e *emailer) SendCustom(to []string, from, subject, header, user, content, footer string) error { + // Use the custom agent for custom emails + return e.customAgent.Send(to, from, subject, header, user, content, footer) +} diff --git a/users/events/streams.go b/users/events/streams.go index 90c3779d31..54db13faef 100644 --- a/users/events/streams.go +++ b/users/events/streams.go @@ -445,6 +445,6 @@ func (es *eventStore) OAuthAddUserPolicy(ctx context.Context, user users.User) e return es.Publish(ctx, addPolicyStream, event) } -func (es *eventStore) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { - return es.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) +func (es *eventStore) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { + return es.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) } diff --git a/users/middleware/authorization.go b/users/middleware/authorization.go index 5af68cc2b8..e1925529bf 100644 --- a/users/middleware/authorization.go +++ b/users/middleware/authorization.go @@ -335,8 +335,8 @@ func (am *authorizationMiddleware) OAuthAddUserPolicy(ctx context.Context, user return am.svc.OAuthAddUserPolicy(ctx, user) } -func (am *authorizationMiddleware) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { - return am.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) +func (am *authorizationMiddleware) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { + return am.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) } func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error { diff --git a/users/middleware/logging.go b/users/middleware/logging.go index 90982fdd52..485c172cd7 100644 --- a/users/middleware/logging.go +++ b/users/middleware/logging.go @@ -522,8 +522,8 @@ func (lm *loggingMiddleware) OAuthAddUserPolicy(ctx context.Context, user users. return lm.svc.OAuthAddUserPolicy(ctx, user) } -// SendEmail logs the send_email request. It logs the recipients and the time it took to complete the request. -func (lm *loggingMiddleware) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) (err error) { +// SendEmailWithUserId logs the send_email request. It logs the recipients and the time it took to complete the request. +func (lm *loggingMiddleware) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) (err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), @@ -538,5 +538,5 @@ func (lm *loggingMiddleware) SendEmail(ctx context.Context, to []string, from, s } lm.logger.Info("Send email completed successfully", args...) }(time.Now()) - return lm.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) + return lm.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) } diff --git a/users/middleware/metrics.go b/users/middleware/metrics.go index a90a711ca5..869d0d365d 100644 --- a/users/middleware/metrics.go +++ b/users/middleware/metrics.go @@ -246,11 +246,11 @@ func (ms *metricsMiddleware) OAuthAddUserPolicy(ctx context.Context, user users. return ms.svc.OAuthAddUserPolicy(ctx, user) } -// SendEmail instruments SendEmail method with metrics. -func (ms *metricsMiddleware) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { +// SendEmailWithUserId instruments SendEmail method with metrics. +func (ms *metricsMiddleware) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { defer func(begin time.Time) { ms.counter.With("method", "send_email").Add(1) ms.latency.With("method", "send_email").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) + return ms.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) } diff --git a/users/middleware/tracing.go b/users/middleware/tracing.go index a9303a4501..a9b22a36e8 100644 --- a/users/middleware/tracing.go +++ b/users/middleware/tracing.go @@ -249,13 +249,13 @@ func (tm *tracingMiddleware) OAuthAddUserPolicy(ctx context.Context, user users. return tm.svc.OAuthAddUserPolicy(ctx, user) } -// SendEmail traces the "SendEmail" operation of the wrapped users.Service. -func (tm *tracingMiddleware) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { +// SendEmailWithUserId traces the "SendEmailWithUserId" operation of the wrapped users.Service. +func (tm *tracingMiddleware) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { ctx, span := tracing.StartSpan(ctx, tm.tracer, "svc_send_email", trace.WithAttributes( attribute.StringSlice("to", to), attribute.String("subject", subject), )) defer span.End() - return tm.svc.SendEmail(ctx, to, from, subject, header, user, content, footer) + return tm.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) } diff --git a/users/mocks/emailer.go b/users/mocks/emailer.go index 387d88b460..cdabbb1640 100644 --- a/users/mocks/emailer.go +++ b/users/mocks/emailer.go @@ -126,6 +126,93 @@ func (_c *Emailer_Send_Call) RunAndReturn(run func(To []string, from string, sub return _c } +// SendCustom provides a mock function for the type Emailer +func (_mock *Emailer) SendCustom(To []string, from string, subject string, header string, user string, content string, footer string) error { + ret := _mock.Called(To, from, subject, header, user, content, footer) + + if len(ret) == 0 { + panic("no return value specified for SendCustom") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func([]string, string, string, string, string, string, string) error); ok { + r0 = returnFunc(To, from, subject, header, user, content, footer) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// Emailer_SendCustom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendCustom' +type Emailer_SendCustom_Call struct { + *mock.Call +} + +// SendCustom is a helper method to define mock.On call +// - To []string +// - from string +// - subject string +// - header string +// - user string +// - content string +// - footer string +func (_e *Emailer_Expecter) SendCustom(To interface{}, from interface{}, subject interface{}, header interface{}, user interface{}, content interface{}, footer interface{}) *Emailer_SendCustom_Call { + return &Emailer_SendCustom_Call{Call: _e.mock.On("SendCustom", To, from, subject, header, user, content, footer)} +} + +func (_c *Emailer_SendCustom_Call) Run(run func(To []string, from string, subject string, header string, user string, content string, footer string)) *Emailer_SendCustom_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []string + if args[0] != nil { + arg0 = args[0].([]string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + var arg4 string + if args[4] != nil { + arg4 = args[4].(string) + } + var arg5 string + if args[5] != nil { + arg5 = args[5].(string) + } + var arg6 string + if args[6] != nil { + arg6 = args[6].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + arg5, + arg6, + ) + }) + return _c +} + +func (_c *Emailer_SendCustom_Call) Return(err error) *Emailer_SendCustom_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Emailer_SendCustom_Call) RunAndReturn(run func(To []string, from string, subject string, header string, user string, content string, footer string) error) *Emailer_SendCustom_Call { + _c.Call.Return(run) + return _c +} + // SendPasswordReset provides a mock function for the type Emailer func (_mock *Emailer) SendPasswordReset(To []string, user string, token string) error { ret := _mock.Called(To, user, token) diff --git a/users/mocks/service.go b/users/mocks/service.go index a012c31316..af0db00514 100644 --- a/users/mocks/service.go +++ b/users/mocks/service.go @@ -867,12 +867,12 @@ func (_c *Service_SearchUsers_Call) RunAndReturn(run func(ctx context.Context, p return _c } -// SendEmail provides a mock function for the type Service -func (_mock *Service) SendEmail(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string) error { +// SendEmailWithUserId provides a mock function for the type Service +func (_mock *Service) SendEmailWithUserId(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string) error { ret := _mock.Called(ctx, to, from, subject, header, user, content, footer) if len(ret) == 0 { - panic("no return value specified for SendEmail") + panic("no return value specified for SendEmailWithUserId") } var r0 error @@ -884,12 +884,12 @@ func (_mock *Service) SendEmail(ctx context.Context, to []string, from string, s return r0 } -// Service_SendEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmail' -type Service_SendEmail_Call struct { +// Service_SendEmailWithUserId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmailWithUserId' +type Service_SendEmailWithUserId_Call struct { *mock.Call } -// SendEmail is a helper method to define mock.On call +// SendEmailWithUserId is a helper method to define mock.On call // - ctx context.Context // - to []string // - from string @@ -898,11 +898,11 @@ type Service_SendEmail_Call struct { // - user string // - content string // - footer string -func (_e *Service_Expecter) SendEmail(ctx interface{}, to interface{}, from interface{}, subject interface{}, header interface{}, user interface{}, content interface{}, footer interface{}) *Service_SendEmail_Call { - return &Service_SendEmail_Call{Call: _e.mock.On("SendEmail", ctx, to, from, subject, header, user, content, footer)} +func (_e *Service_Expecter) SendEmailWithUserId(ctx interface{}, to interface{}, from interface{}, subject interface{}, header interface{}, user interface{}, content interface{}, footer interface{}) *Service_SendEmailWithUserId_Call { + return &Service_SendEmailWithUserId_Call{Call: _e.mock.On("SendEmailWithUserId", ctx, to, from, subject, header, user, content, footer)} } -func (_c *Service_SendEmail_Call) Run(run func(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string)) *Service_SendEmail_Call { +func (_c *Service_SendEmailWithUserId_Call) Run(run func(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string)) *Service_SendEmailWithUserId_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -950,12 +950,12 @@ func (_c *Service_SendEmail_Call) Run(run func(ctx context.Context, to []string, return _c } -func (_c *Service_SendEmail_Call) Return(err error) *Service_SendEmail_Call { +func (_c *Service_SendEmailWithUserId_Call) Return(err error) *Service_SendEmailWithUserId_Call { _c.Call.Return(err) return _c } -func (_c *Service_SendEmail_Call) RunAndReturn(run func(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string) error) *Service_SendEmail_Call { +func (_c *Service_SendEmailWithUserId_Call) RunAndReturn(run func(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string) error) *Service_SendEmailWithUserId_Call { _c.Call.Return(run) return _c } diff --git a/users/mocks/users_service_client.go b/users/mocks/users_service_client.go new file mode 100644 index 0000000000..3fe71665a2 --- /dev/null +++ b/users/mocks/users_service_client.go @@ -0,0 +1,127 @@ +// Copyright (c) Abstract Machines + +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/absmach/supermq/api/grpc/users/v1" + mock "github.com/stretchr/testify/mock" + "google.golang.org/grpc" +) + +// NewUsersServiceClient creates a new instance of UsersServiceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUsersServiceClient(t interface { + mock.TestingT + Cleanup(func()) +}) *UsersServiceClient { + mock := &UsersServiceClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// UsersServiceClient is an autogenerated mock type for the UsersServiceClient type +type UsersServiceClient struct { + mock.Mock +} + +type UsersServiceClient_Expecter struct { + mock *mock.Mock +} + +func (_m *UsersServiceClient) EXPECT() *UsersServiceClient_Expecter { + return &UsersServiceClient_Expecter{mock: &_m.Mock} +} + +// SendEmailWithUserId provides a mock function for the type UsersServiceClient +func (_mock *UsersServiceClient) SendEmailWithUserId(ctx context.Context, in *v1.SendEmailWithUserIdReq, opts ...grpc.CallOption) (*v1.SendEmailRes, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, in, opts) + } else { + tmpRet = _mock.Called(ctx, in) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for SendEmailWithUserId") + } + + var r0 *v1.SendEmailRes + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.SendEmailWithUserIdReq, ...grpc.CallOption) (*v1.SendEmailRes, error)); ok { + return returnFunc(ctx, in, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.SendEmailWithUserIdReq, ...grpc.CallOption) *v1.SendEmailRes); ok { + r0 = returnFunc(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.SendEmailRes) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, *v1.SendEmailWithUserIdReq, ...grpc.CallOption) error); ok { + r1 = returnFunc(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// UsersServiceClient_SendEmailWithUserId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmailWithUserId' +type UsersServiceClient_SendEmailWithUserId_Call struct { + *mock.Call +} + +// SendEmailWithUserId is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.SendEmailWithUserIdReq +// - opts ...grpc.CallOption +func (_e *UsersServiceClient_Expecter) SendEmailWithUserId(ctx interface{}, in interface{}, opts ...interface{}) *UsersServiceClient_SendEmailWithUserId_Call { + return &UsersServiceClient_SendEmailWithUserId_Call{Call: _e.mock.On("SendEmailWithUserId", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *UsersServiceClient_SendEmailWithUserId_Call) Run(run func(ctx context.Context, in *v1.SendEmailWithUserIdReq, opts ...grpc.CallOption)) *UsersServiceClient_SendEmailWithUserId_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *v1.SendEmailWithUserIdReq + if args[1] != nil { + arg1 = args[1].(*v1.SendEmailWithUserIdReq) + } + var arg2 []grpc.CallOption + var variadicArgs []grpc.CallOption + if len(args) > 2 { + variadicArgs = args[2].([]grpc.CallOption) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *UsersServiceClient_SendEmailWithUserId_Call) Return(sendEmailRes *v1.SendEmailRes, err error) *UsersServiceClient_SendEmailWithUserId_Call { + _c.Call.Return(sendEmailRes, err) + return _c +} + +func (_c *UsersServiceClient_SendEmailWithUserId_Call) RunAndReturn(run func(ctx context.Context, in *v1.SendEmailWithUserIdReq, opts ...grpc.CallOption) (*v1.SendEmailRes, error)) *UsersServiceClient_SendEmailWithUserId_Call { + _c.Call.Return(run) + return _c +} diff --git a/users/service.go b/users/service.go index 67dc2b79e3..5a5f044b8d 100644 --- a/users/service.go +++ b/users/service.go @@ -815,6 +815,20 @@ func changed(updated *string, old string) bool { return *updated != old } -func (svc service) SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { - return svc.email.Send(to, from, subject, header, user, content, footer) +func (svc service) SendEmailWithUserId(ctx context.Context, userIds []string, from, subject, header, user, content, footer string) error { + for i, userId := range userIds { + u, err := svc.users.RetrieveByID(ctx, userId) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + + userIds[i] = u.Email + } + + inviter, err := svc.users.RetrieveByID(ctx, from) + if err != nil { + return err + } + + return svc.email.SendCustom(userIds, inviter.FirstName+" "+inviter.LastName, subject, header, user, content, footer) } diff --git a/users/users.go b/users/users.go index 3381553656..21952e3227 100644 --- a/users/users.go +++ b/users/users.go @@ -244,6 +244,6 @@ type Service interface { // OAuthAddUserPolicy adds a policy to the user for an OAuth request. OAuthAddUserPolicy(ctx context.Context, user User) error - // SendEmail sends an email using the email agent. - SendEmail(ctx context.Context, to []string, from, subject, header, user, content, footer string) error + // SendEmailWithUserId sends an email using the email agent. + SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error } From 238bde03a56db15807c907acb2ed54380881fef8 Mon Sep 17 00:00:00 2001 From: WashingtonKK Date: Mon, 3 Nov 2025 11:27:42 +0300 Subject: [PATCH 3/9] Refactor email service to use dedicated EmailService gRPC API Signed-off-by: WashingtonKK --- api/grpc/emails/v1/emails.pb.go | 311 ++++++++++++++++++++++++++ api/grpc/emails/v1/emails_grpc.pb.go | 130 +++++++++++ api/grpc/users/v1/users.pb.go | 230 ------------------- api/grpc/users/v1/users_grpc.pb.go | 130 ----------- channels/api/grpc/endpoint_test.go | 68 +++--- clients/api/grpc/endpoint_test.go | 84 ++++--- cmd/domains/main.go | 6 +- cmd/users/main.go | 4 +- domains/service.go | 20 +- domains/service_test.go | 6 +- groups/api/grpc/endpoint_test.go | 36 ++- internal/proto/emails/v1/emails.proto | 34 +++ internal/proto/users/v1/users.proto | 27 --- pkg/grpcclient/client.go | 4 +- tools/config/.mockery.yaml | 4 +- users/api/grpc/client.go | 46 ++-- users/api/grpc/server.go | 27 ++- users/mocks/email_service_client.go | 127 +++++++++++ users/mocks/users_service_client.go | 127 ----------- 19 files changed, 761 insertions(+), 660 deletions(-) create mode 100644 api/grpc/emails/v1/emails.pb.go create mode 100644 api/grpc/emails/v1/emails_grpc.pb.go delete mode 100644 api/grpc/users/v1/users.pb.go delete mode 100644 api/grpc/users/v1/users_grpc.pb.go create mode 100644 internal/proto/emails/v1/emails.proto delete mode 100644 internal/proto/users/v1/users.proto create mode 100644 users/mocks/email_service_client.go delete mode 100644 users/mocks/users_service_client.go diff --git a/api/grpc/emails/v1/emails.pb.go b/api/grpc/emails/v1/emails.pb.go new file mode 100644 index 0000000000..e9d0876696 --- /dev/null +++ b/api/grpc/emails/v1/emails.pb.go @@ -0,0 +1,311 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.33.0 +// source: emails/v1/emails.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ContactType int32 + +const ( + ContactType_ID ContactType = 0 + ContactType_Email ContactType = 1 +) + +// Enum value maps for ContactType. +var ( + ContactType_name = map[int32]string{ + 0: "ID", + 1: "Email", + } + ContactType_value = map[string]int32{ + "ID": 0, + "Email": 1, + } +) + +func (x ContactType) Enum() *ContactType { + p := new(ContactType) + *p = x + return p +} + +func (x ContactType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ContactType) Descriptor() protoreflect.EnumDescriptor { + return file_emails_v1_emails_proto_enumTypes[0].Descriptor() +} + +func (ContactType) Type() protoreflect.EnumType { + return &file_emails_v1_emails_proto_enumTypes[0] +} + +func (x ContactType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ContactType.Descriptor instead. +func (ContactType) EnumDescriptor() ([]byte, []int) { + return file_emails_v1_emails_proto_rawDescGZIP(), []int{0} +} + +type EmailReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + From string `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` // Sender + FromType ContactType `protobuf:"varint,2,opt,name=fromType,proto3,enum=emails.v1.ContactType" json:"fromType,omitempty"` // indicate sender id or email + Tos []string `protobuf:"bytes,3,rep,name=tos,proto3" json:"tos,omitempty"` // recipients + ToType ContactType `protobuf:"varint,4,opt,name=toType,proto3,enum=emails.v1.ContactType" json:"toType,omitempty"` // indicate recipient id or email + Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"` // Email subject + Content string `protobuf:"bytes,6,opt,name=content,proto3" json:"content,omitempty"` // Email content/URL + Template *string `protobuf:"bytes,10,opt,name=template,proto3,oneof" json:"template,omitempty"` // the whole template to be parsed by emailer + TemplateFile *string `protobuf:"bytes,11,opt,name=template_file,json=templateFile,proto3,oneof" json:"template_file,omitempty"` // template file name to be parsed by emailer + Options map[string]string `protobuf:"bytes,12,rep,name=options,proto3" json:"options,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // map that can be used in template + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmailReq) Reset() { + *x = EmailReq{} + mi := &file_emails_v1_emails_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmailReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmailReq) ProtoMessage() {} + +func (x *EmailReq) ProtoReflect() protoreflect.Message { + mi := &file_emails_v1_emails_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmailReq.ProtoReflect.Descriptor instead. +func (*EmailReq) Descriptor() ([]byte, []int) { + return file_emails_v1_emails_proto_rawDescGZIP(), []int{0} +} + +func (x *EmailReq) GetFrom() string { + if x != nil { + return x.From + } + return "" +} + +func (x *EmailReq) GetFromType() ContactType { + if x != nil { + return x.FromType + } + return ContactType_ID +} + +func (x *EmailReq) GetTos() []string { + if x != nil { + return x.Tos + } + return nil +} + +func (x *EmailReq) GetToType() ContactType { + if x != nil { + return x.ToType + } + return ContactType_ID +} + +func (x *EmailReq) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *EmailReq) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *EmailReq) GetTemplate() string { + if x != nil && x.Template != nil { + return *x.Template + } + return "" +} + +func (x *EmailReq) GetTemplateFile() string { + if x != nil && x.TemplateFile != nil { + return *x.TemplateFile + } + return "" +} + +func (x *EmailReq) GetOptions() map[string]string { + if x != nil { + return x.Options + } + return nil +} + +type SendEmailRes struct { + state protoimpl.MessageState `protogen:"open.v1"` + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendEmailRes) Reset() { + *x = SendEmailRes{} + mi := &file_emails_v1_emails_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendEmailRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendEmailRes) ProtoMessage() {} + +func (x *SendEmailRes) ProtoReflect() protoreflect.Message { + mi := &file_emails_v1_emails_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendEmailRes.ProtoReflect.Descriptor instead. +func (*SendEmailRes) Descriptor() ([]byte, []int) { + return file_emails_v1_emails_proto_rawDescGZIP(), []int{1} +} + +func (x *SendEmailRes) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +var File_emails_v1_emails_proto protoreflect.FileDescriptor + +const file_emails_v1_emails_proto_rawDesc = "" + + "\n" + + "\x16emails/v1/emails.proto\x12\temails.v1\"\xaa\x03\n" + + "\bEmailReq\x12\x12\n" + + "\x04from\x18\x01 \x01(\tR\x04from\x122\n" + + "\bfromType\x18\x02 \x01(\x0e2\x16.emails.v1.ContactTypeR\bfromType\x12\x10\n" + + "\x03tos\x18\x03 \x03(\tR\x03tos\x12.\n" + + "\x06toType\x18\x04 \x01(\x0e2\x16.emails.v1.ContactTypeR\x06toType\x12\x18\n" + + "\asubject\x18\x05 \x01(\tR\asubject\x12\x18\n" + + "\acontent\x18\x06 \x01(\tR\acontent\x12\x1f\n" + + "\btemplate\x18\n" + + " \x01(\tH\x00R\btemplate\x88\x01\x01\x12(\n" + + "\rtemplate_file\x18\v \x01(\tH\x01R\ftemplateFile\x88\x01\x01\x12:\n" + + "\aoptions\x18\f \x03(\v2 .emails.v1.EmailReq.OptionsEntryR\aoptions\x1a:\n" + + "\fOptionsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\v\n" + + "\t_templateB\x10\n" + + "\x0e_template_file\"$\n" + + "\fSendEmailRes\x12\x14\n" + + "\x05error\x18\x01 \x01(\tR\x05error* \n" + + "\vContactType\x12\x06\n" + + "\x02ID\x10\x00\x12\t\n" + + "\x05Email\x10\x012K\n" + + "\fEmailService\x12;\n" + + "\tSendEmail\x12\x13.emails.v1.EmailReq\x1a\x17.emails.v1.SendEmailRes\"\x00B/Z-github.com/absmach/supermq/api/grpc/emails/v1b\x06proto3" + +var ( + file_emails_v1_emails_proto_rawDescOnce sync.Once + file_emails_v1_emails_proto_rawDescData []byte +) + +func file_emails_v1_emails_proto_rawDescGZIP() []byte { + file_emails_v1_emails_proto_rawDescOnce.Do(func() { + file_emails_v1_emails_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_emails_v1_emails_proto_rawDesc), len(file_emails_v1_emails_proto_rawDesc))) + }) + return file_emails_v1_emails_proto_rawDescData +} + +var file_emails_v1_emails_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_emails_v1_emails_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_emails_v1_emails_proto_goTypes = []any{ + (ContactType)(0), // 0: emails.v1.ContactType + (*EmailReq)(nil), // 1: emails.v1.EmailReq + (*SendEmailRes)(nil), // 2: emails.v1.SendEmailRes + nil, // 3: emails.v1.EmailReq.OptionsEntry +} +var file_emails_v1_emails_proto_depIdxs = []int32{ + 0, // 0: emails.v1.EmailReq.fromType:type_name -> emails.v1.ContactType + 0, // 1: emails.v1.EmailReq.toType:type_name -> emails.v1.ContactType + 3, // 2: emails.v1.EmailReq.options:type_name -> emails.v1.EmailReq.OptionsEntry + 1, // 3: emails.v1.EmailService.SendEmail:input_type -> emails.v1.EmailReq + 2, // 4: emails.v1.EmailService.SendEmail:output_type -> emails.v1.SendEmailRes + 4, // [4:5] is the sub-list for method output_type + 3, // [3:4] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_emails_v1_emails_proto_init() } +func file_emails_v1_emails_proto_init() { + if File_emails_v1_emails_proto != nil { + return + } + file_emails_v1_emails_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_emails_v1_emails_proto_rawDesc), len(file_emails_v1_emails_proto_rawDesc)), + NumEnums: 1, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_emails_v1_emails_proto_goTypes, + DependencyIndexes: file_emails_v1_emails_proto_depIdxs, + EnumInfos: file_emails_v1_emails_proto_enumTypes, + MessageInfos: file_emails_v1_emails_proto_msgTypes, + }.Build() + File_emails_v1_emails_proto = out.File + file_emails_v1_emails_proto_goTypes = nil + file_emails_v1_emails_proto_depIdxs = nil +} diff --git a/api/grpc/emails/v1/emails_grpc.pb.go b/api/grpc/emails/v1/emails_grpc.pb.go new file mode 100644 index 0000000000..cc207d00f1 --- /dev/null +++ b/api/grpc/emails/v1/emails_grpc.pb.go @@ -0,0 +1,130 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.33.0 +// source: emails/v1/emails.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + EmailService_SendEmail_FullMethodName = "/emails.v1.EmailService/SendEmail" +) + +// EmailServiceClient is the client API for EmailService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// EmailService is a service that provides email-related functionalities +// for SuperMQ services. +type EmailServiceClient interface { + SendEmail(ctx context.Context, in *EmailReq, opts ...grpc.CallOption) (*SendEmailRes, error) +} + +type emailServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewEmailServiceClient(cc grpc.ClientConnInterface) EmailServiceClient { + return &emailServiceClient{cc} +} + +func (c *emailServiceClient) SendEmail(ctx context.Context, in *EmailReq, opts ...grpc.CallOption) (*SendEmailRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SendEmailRes) + err := c.cc.Invoke(ctx, EmailService_SendEmail_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// EmailServiceServer is the server API for EmailService service. +// All implementations must embed UnimplementedEmailServiceServer +// for forward compatibility. +// +// EmailService is a service that provides email-related functionalities +// for SuperMQ services. +type EmailServiceServer interface { + SendEmail(context.Context, *EmailReq) (*SendEmailRes, error) + mustEmbedUnimplementedEmailServiceServer() +} + +// UnimplementedEmailServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedEmailServiceServer struct{} + +func (UnimplementedEmailServiceServer) SendEmail(context.Context, *EmailReq) (*SendEmailRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendEmail not implemented") +} +func (UnimplementedEmailServiceServer) mustEmbedUnimplementedEmailServiceServer() {} +func (UnimplementedEmailServiceServer) testEmbeddedByValue() {} + +// UnsafeEmailServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to EmailServiceServer will +// result in compilation errors. +type UnsafeEmailServiceServer interface { + mustEmbedUnimplementedEmailServiceServer() +} + +func RegisterEmailServiceServer(s grpc.ServiceRegistrar, srv EmailServiceServer) { + // If the following call pancis, it indicates UnimplementedEmailServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&EmailService_ServiceDesc, srv) +} + +func _EmailService_SendEmail_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EmailReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EmailServiceServer).SendEmail(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: EmailService_SendEmail_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EmailServiceServer).SendEmail(ctx, req.(*EmailReq)) + } + return interceptor(ctx, in, info, handler) +} + +// EmailService_ServiceDesc is the grpc.ServiceDesc for EmailService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var EmailService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "emails.v1.EmailService", + HandlerType: (*EmailServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SendEmail", + Handler: _EmailService_SendEmail_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "emails/v1/emails.proto", +} diff --git a/api/grpc/users/v1/users.pb.go b/api/grpc/users/v1/users.pb.go deleted file mode 100644 index be933374bb..0000000000 --- a/api/grpc/users/v1/users.pb.go +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 -// source: users/v1/users.proto - -package v1 - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type SendEmailWithUserIdReq struct { - state protoimpl.MessageState `protogen:"open.v1"` - Users []string `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` // Email recipients - From string `protobuf:"bytes,2,opt,name=from,proto3" json:"from,omitempty"` // Sender email address (optional) - Subject string `protobuf:"bytes,3,opt,name=subject,proto3" json:"subject,omitempty"` // Email subject - Header string `protobuf:"bytes,4,opt,name=header,proto3" json:"header,omitempty"` // Email header text - User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` // User name - Content string `protobuf:"bytes,6,opt,name=content,proto3" json:"content,omitempty"` // Email content/URL - Footer string `protobuf:"bytes,7,opt,name=footer,proto3" json:"footer,omitempty"` // Email footer text - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SendEmailWithUserIdReq) Reset() { - *x = SendEmailWithUserIdReq{} - mi := &file_users_v1_users_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SendEmailWithUserIdReq) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SendEmailWithUserIdReq) ProtoMessage() {} - -func (x *SendEmailWithUserIdReq) ProtoReflect() protoreflect.Message { - mi := &file_users_v1_users_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SendEmailWithUserIdReq.ProtoReflect.Descriptor instead. -func (*SendEmailWithUserIdReq) Descriptor() ([]byte, []int) { - return file_users_v1_users_proto_rawDescGZIP(), []int{0} -} - -func (x *SendEmailWithUserIdReq) GetUsers() []string { - if x != nil { - return x.Users - } - return nil -} - -func (x *SendEmailWithUserIdReq) GetFrom() string { - if x != nil { - return x.From - } - return "" -} - -func (x *SendEmailWithUserIdReq) GetSubject() string { - if x != nil { - return x.Subject - } - return "" -} - -func (x *SendEmailWithUserIdReq) GetHeader() string { - if x != nil { - return x.Header - } - return "" -} - -func (x *SendEmailWithUserIdReq) GetUser() string { - if x != nil { - return x.User - } - return "" -} - -func (x *SendEmailWithUserIdReq) GetContent() string { - if x != nil { - return x.Content - } - return "" -} - -func (x *SendEmailWithUserIdReq) GetFooter() string { - if x != nil { - return x.Footer - } - return "" -} - -type SendEmailRes struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sent bool `protobuf:"varint,1,opt,name=sent,proto3" json:"sent,omitempty"` // Whether the email was sent successfully - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SendEmailRes) Reset() { - *x = SendEmailRes{} - mi := &file_users_v1_users_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SendEmailRes) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SendEmailRes) ProtoMessage() {} - -func (x *SendEmailRes) ProtoReflect() protoreflect.Message { - mi := &file_users_v1_users_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SendEmailRes.ProtoReflect.Descriptor instead. -func (*SendEmailRes) Descriptor() ([]byte, []int) { - return file_users_v1_users_proto_rawDescGZIP(), []int{1} -} - -func (x *SendEmailRes) GetSent() bool { - if x != nil { - return x.Sent - } - return false -} - -var File_users_v1_users_proto protoreflect.FileDescriptor - -const file_users_v1_users_proto_rawDesc = "" + - "\n" + - "\x14users/v1/users.proto\x12\busers.v1\"\xba\x01\n" + - "\x16SendEmailWithUserIdReq\x12\x14\n" + - "\x05users\x18\x01 \x03(\tR\x05users\x12\x12\n" + - "\x04from\x18\x02 \x01(\tR\x04from\x12\x18\n" + - "\asubject\x18\x03 \x01(\tR\asubject\x12\x16\n" + - "\x06header\x18\x04 \x01(\tR\x06header\x12\x12\n" + - "\x04user\x18\x05 \x01(\tR\x04user\x12\x18\n" + - "\acontent\x18\x06 \x01(\tR\acontent\x12\x16\n" + - "\x06footer\x18\a \x01(\tR\x06footer\"\"\n" + - "\fSendEmailRes\x12\x12\n" + - "\x04sent\x18\x01 \x01(\bR\x04sent2a\n" + - "\fUsersService\x12Q\n" + - "\x13SendEmailWithUserId\x12 .users.v1.SendEmailWithUserIdReq\x1a\x16.users.v1.SendEmailRes\"\x00B.Z,github.com/absmach/supermq/api/grpc/users/v1b\x06proto3" - -var ( - file_users_v1_users_proto_rawDescOnce sync.Once - file_users_v1_users_proto_rawDescData []byte -) - -func file_users_v1_users_proto_rawDescGZIP() []byte { - file_users_v1_users_proto_rawDescOnce.Do(func() { - file_users_v1_users_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_users_v1_users_proto_rawDesc), len(file_users_v1_users_proto_rawDesc))) - }) - return file_users_v1_users_proto_rawDescData -} - -var file_users_v1_users_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_users_v1_users_proto_goTypes = []any{ - (*SendEmailWithUserIdReq)(nil), // 0: users.v1.SendEmailWithUserIdReq - (*SendEmailRes)(nil), // 1: users.v1.SendEmailRes -} -var file_users_v1_users_proto_depIdxs = []int32{ - 0, // 0: users.v1.UsersService.SendEmailWithUserId:input_type -> users.v1.SendEmailWithUserIdReq - 1, // 1: users.v1.UsersService.SendEmailWithUserId:output_type -> users.v1.SendEmailRes - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_users_v1_users_proto_init() } -func file_users_v1_users_proto_init() { - if File_users_v1_users_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_users_v1_users_proto_rawDesc), len(file_users_v1_users_proto_rawDesc)), - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_users_v1_users_proto_goTypes, - DependencyIndexes: file_users_v1_users_proto_depIdxs, - MessageInfos: file_users_v1_users_proto_msgTypes, - }.Build() - File_users_v1_users_proto = out.File - file_users_v1_users_proto_goTypes = nil - file_users_v1_users_proto_depIdxs = nil -} diff --git a/api/grpc/users/v1/users_grpc.pb.go b/api/grpc/users/v1/users_grpc.pb.go deleted file mode 100644 index 8b6b0bd265..0000000000 --- a/api/grpc/users/v1/users_grpc.pb.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v6.33.0 -// source: users/v1/users.proto - -package v1 - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - UsersService_SendEmailWithUserId_FullMethodName = "/users.v1.UsersService/SendEmailWithUserId" -) - -// UsersServiceClient is the client API for UsersService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -// -// UsersService is a service that provides user-related functionalities -// for SuperMQ services. -type UsersServiceClient interface { - SendEmailWithUserId(ctx context.Context, in *SendEmailWithUserIdReq, opts ...grpc.CallOption) (*SendEmailRes, error) -} - -type usersServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewUsersServiceClient(cc grpc.ClientConnInterface) UsersServiceClient { - return &usersServiceClient{cc} -} - -func (c *usersServiceClient) SendEmailWithUserId(ctx context.Context, in *SendEmailWithUserIdReq, opts ...grpc.CallOption) (*SendEmailRes, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(SendEmailRes) - err := c.cc.Invoke(ctx, UsersService_SendEmailWithUserId_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// UsersServiceServer is the server API for UsersService service. -// All implementations must embed UnimplementedUsersServiceServer -// for forward compatibility. -// -// UsersService is a service that provides user-related functionalities -// for SuperMQ services. -type UsersServiceServer interface { - SendEmailWithUserId(context.Context, *SendEmailWithUserIdReq) (*SendEmailRes, error) - mustEmbedUnimplementedUsersServiceServer() -} - -// UnimplementedUsersServiceServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedUsersServiceServer struct{} - -func (UnimplementedUsersServiceServer) SendEmailWithUserId(context.Context, *SendEmailWithUserIdReq) (*SendEmailRes, error) { - return nil, status.Errorf(codes.Unimplemented, "method SendEmailWithUserId not implemented") -} -func (UnimplementedUsersServiceServer) mustEmbedUnimplementedUsersServiceServer() {} -func (UnimplementedUsersServiceServer) testEmbeddedByValue() {} - -// UnsafeUsersServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to UsersServiceServer will -// result in compilation errors. -type UnsafeUsersServiceServer interface { - mustEmbedUnimplementedUsersServiceServer() -} - -func RegisterUsersServiceServer(s grpc.ServiceRegistrar, srv UsersServiceServer) { - // If the following call pancis, it indicates UnimplementedUsersServiceServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&UsersService_ServiceDesc, srv) -} - -func _UsersService_SendEmailWithUserId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SendEmailWithUserIdReq) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(UsersServiceServer).SendEmailWithUserId(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: UsersService_SendEmailWithUserId_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(UsersServiceServer).SendEmailWithUserId(ctx, req.(*SendEmailWithUserIdReq)) - } - return interceptor(ctx, in, info, handler) -} - -// UsersService_ServiceDesc is the grpc.ServiceDesc for UsersService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var UsersService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "users.v1.UsersService", - HandlerType: (*UsersServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "SendEmailWithUserId", - Handler: _UsersService_SendEmailWithUserId_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "users/v1/users.proto", -} diff --git a/channels/api/grpc/endpoint_test.go b/channels/api/grpc/endpoint_test.go index 4014474d39..761caa1ac5 100644 --- a/channels/api/grpc/endpoint_test.go +++ b/channels/api/grpc/endpoint_test.go @@ -29,8 +29,6 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -const port = 0 - var ( validID = testsutil.GenerateUUID(&testing.T{}) validChannel = ch.Channel{ @@ -41,26 +39,26 @@ var ( ) func startGRPCServer(svc *mocks.Service) (*grpc.Server, int) { - listener, err := net.Listen("tcp", ":0") - if err != nil { - panic(fmt.Sprintf("failed to obtain port: %s", err)) - } - server := grpc.NewServer() - grpcChannelsV1.RegisterChannelsServiceServer(server, grpcapi.NewServer(svc)) - go func() { - if err := server.Serve(listener); err != nil { - panic(fmt.Sprintf("failed to serve: %s", err)) - } - }() - p := listener.Addr().(*net.TCPAddr).Port - return server, p + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(fmt.Sprintf("failed to obtain port: %s", err)) + } + server := grpc.NewServer() + grpcChannelsV1.RegisterChannelsServiceServer(server, grpcapi.NewServer(svc)) + go func() { + if err := server.Serve(listener); err != nil { + panic(fmt.Sprintf("failed to serve: %s", err)) + } + }() + p := listener.Addr().(*net.TCPAddr).Port + return server, p } func TestAuthorize(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -135,10 +133,10 @@ func TestAuthorize(t *testing.T) { } func TestRemoveClientConnections(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -174,10 +172,10 @@ func TestRemoveClientConnections(t *testing.T) { } func TestUnsetParentGroupFromChannelsEndpoint(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -218,10 +216,10 @@ func TestUnsetParentGroupFromChannelsEndpoint(t *testing.T) { } func TestRetrieveEntity(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -269,10 +267,10 @@ func TestRetrieveEntity(t *testing.T) { } func TestRetrieveIDByRoute(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) diff --git a/clients/api/grpc/endpoint_test.go b/clients/api/grpc/endpoint_test.go index 2e18fb9372..4692fa7258 100644 --- a/clients/api/grpc/endpoint_test.go +++ b/clients/api/grpc/endpoint_test.go @@ -25,8 +25,6 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -const port = 0 - var ( validID = testsutil.GenerateUUID(&testing.T{}) validSecret = "validSecret" @@ -39,26 +37,26 @@ var ( ) func startGRPCServer(svc *mocks.Service) (*grpc.Server, int) { - listener, err := net.Listen("tcp", ":0") - if err != nil { - panic(fmt.Sprintf("failed to obtain port: %s", err)) - } - server := grpc.NewServer() - grpcClientsV1.RegisterClientsServiceServer(server, grpcapi.NewServer(svc)) - go func() { - if err := server.Serve(listener); err != nil { - panic(fmt.Sprintf("failed to serve: %s", err)) - } - }() - p := listener.Addr().(*net.TCPAddr).Port - return server, p + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(fmt.Sprintf("failed to obtain port: %s", err)) + } + server := grpc.NewServer() + grpcClientsV1.RegisterClientsServiceServer(server, grpcapi.NewServer(svc)) + go func() { + if err := server.Serve(listener); err != nil { + panic(fmt.Sprintf("failed to serve: %s", err)) + } + }() + p := listener.Addr().(*net.TCPAddr).Port + return server, p } func TestAuthenticate(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -106,10 +104,10 @@ func TestAuthenticate(t *testing.T) { } func TestRetrieveEntity(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -162,10 +160,10 @@ func TestRetrieveEntity(t *testing.T) { } func TestRetrieveEntities(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -229,10 +227,10 @@ func TestRetrieveEntities(t *testing.T) { } func TestAddConnections(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -284,10 +282,10 @@ func TestAddConnections(t *testing.T) { } func TestRemoveConnections(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -339,10 +337,10 @@ func TestRemoveConnections(t *testing.T) { } func TestRemoveChannelConnections(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -380,10 +378,10 @@ func TestRemoveChannelConnections(t *testing.T) { } func TestUnsetParentGroupFromClient(t *testing.T) { - svc := new(mocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - authAddr := fmt.Sprintf("localhost:%d", p) + svc := new(mocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + authAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) diff --git a/cmd/domains/main.go b/cmd/domains/main.go index 43548b68f4..c0779658eb 100644 --- a/cmd/domains/main.go +++ b/cmd/domains/main.go @@ -15,7 +15,7 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/supermq" grpcDomainsV1 "github.com/absmach/supermq/api/grpc/domains/v1" - grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" "github.com/absmach/supermq/domains" domainsSvc "github.com/absmach/supermq/domains" domainsgrpcapi "github.com/absmach/supermq/domains/api/grpc" @@ -278,7 +278,7 @@ func main() { } } -func newDomainService(ctx context.Context, domainsRepo domainsSvc.Repository, cache domainsSvc.Cache, tracer trace.Tracer, cfg config, authz authz.Authorization, policiessvc policies.Service, logger *slog.Logger, callout callout.Callout, usersClient grpcUsersV1.UsersServiceClient) (domains.Service, error) { +func newDomainService(ctx context.Context, domainsRepo domainsSvc.Repository, cache domainsSvc.Cache, tracer trace.Tracer, cfg config, authz authz.Authorization, policiessvc policies.Service, logger *slog.Logger, callout callout.Callout, emailClient grpcEmailsV1.EmailServiceClient) (domains.Service, error) { idProvider := uuid.New() sidProvider, err := sid.New() if err != nil { @@ -290,7 +290,7 @@ func newDomainService(ctx context.Context, domainsRepo domainsSvc.Repository, ca return nil, err } - svc, err := domainsSvc.New(domainsRepo, cache, policiessvc, idProvider, sidProvider, availableActions, builtInRoles, usersClient) + svc, err := domainsSvc.New(domainsRepo, cache, policiessvc, idProvider, sidProvider, availableActions, builtInRoles, emailClient) if err != nil { return nil, fmt.Errorf("failed to init domain service: %w", err) } diff --git a/cmd/users/main.go b/cmd/users/main.go index 68d17cede7..4cb42de2af 100644 --- a/cmd/users/main.go +++ b/cmd/users/main.go @@ -17,8 +17,8 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/supermq" grpcDomainsV1 "github.com/absmach/supermq/api/grpc/domains/v1" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" - grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" "github.com/absmach/supermq/internal/email" smqlog "github.com/absmach/supermq/logger" smqauthn "github.com/absmach/supermq/pkg/authn" @@ -257,7 +257,7 @@ func main() { } registerUsersServiceServer := func(srv *grpc.Server) { reflection.Register(srv) - grpcUsersV1.RegisterUsersServiceServer(srv, usersgrpcapi.NewUsersServer(csvc)) + grpcEmailsV1.RegisterEmailServiceServer(srv, usersgrpcapi.NewUsersServer(csvc)) } gs := grpcserver.NewServer(ctx, cancel, svcName, grpcServerConfig, registerUsersServiceServer, logger) diff --git a/domains/service.go b/domains/service.go index 0c7fb760e4..e4b4999859 100644 --- a/domains/service.go +++ b/domains/service.go @@ -8,7 +8,7 @@ import ( "time" "github.com/absmach/supermq" - grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/pkg/errors" repoerr "github.com/absmach/supermq/pkg/errors/repository" @@ -27,13 +27,13 @@ type service struct { cache Cache policy policies.Service idProvider supermq.IDProvider - usersClient grpcUsersV1.UsersServiceClient + usersClient grpcEmailsV1.EmailServiceClient roles.ProvisionManageService } var _ Service = (*service)(nil) -func New(repo Repository, cache Cache, policy policies.Service, idProvider supermq.IDProvider, sidProvider supermq.IDProvider, availableActions []roles.Action, builtInRoles map[roles.BuiltInRoleName][]roles.Action, usersClient grpcUsersV1.UsersServiceClient) (Service, error) { +func New(repo Repository, cache Cache, policy policies.Service, idProvider supermq.IDProvider, sidProvider supermq.IDProvider, availableActions []roles.Action, builtInRoles map[roles.BuiltInRoleName][]roles.Action, usersClient grpcEmailsV1.EmailServiceClient) (Service, error) { rpms, err := roles.NewProvisionManageService(policies.DomainType, repo, policy, sidProvider, availableActions, builtInRoles) if err != nil { return nil, err @@ -209,11 +209,15 @@ func (svc *service) SendInvitation(ctx context.Context, session authn.Session, i return errors.Wrap(svcerr.ErrCreateEntity, err) } - svc.usersClient.SendEmailWithUserId(ctx, &grpcUsersV1.SendEmailWithUserIdReq{ - Users: []string{invitation.InviteeUserID}, - Subject: "Invitation to join Domain", - From: invitation.InvitedBy, - }) + if _, err := svc.usersClient.SendEmail(ctx, &grpcEmailsV1.EmailReq{ + Tos: []string{invitation.InviteeUserID}, + ToType: grpcEmailsV1.ContactType_ID, + Subject: "Invitation to join Domain", + From: invitation.InvitedBy, + FromType: grpcEmailsV1.ContactType_ID, + }); err != nil { + return err + } return nil } diff --git a/domains/service_test.go b/domains/service_test.go index b31f742e56..fb41cfc0d9 100644 --- a/domains/service_test.go +++ b/domains/service_test.go @@ -86,7 +86,7 @@ var ( drepo *mocks.Repository dcache *mocks.Cache policy *policiesMocks.Service - usersClient *uMocks.UsersServiceClient + usersClient *uMocks.EmailServiceClient ) func newService() domains.Service { @@ -95,7 +95,7 @@ func newService() domains.Service { idProvider := uuid.NewMock() sidProvider := sid.NewMock() policy = new(policiesMocks.Service) - usersClient = &uMocks.UsersServiceClient{} + usersClient = &uMocks.EmailServiceClient{} availableActions := []roles.Action{} builtInRoles := map[roles.BuiltInRoleName][]roles.Action{ groups.BuiltInRoleAdmin: availableActions, @@ -702,7 +702,7 @@ func TestSendInvitation(t *testing.T) { repoCall1 := drepo.On("SaveInvitation", context.Background(), mock.Anything).Return(tc.createInvitationErr) repoCall2 := drepo.On("RetrieveInvitation", context.Background(), tc.req.InviteeUserID, tc.req.DomainID).Return(tc.retrieveInvRes, tc.retrieveInvErr) repoCall3 := drepo.On("UpdateRejection", context.Background(), mock.Anything).Return(tc.updateRejectionErr) - usersClientCall := usersClient.On("SendEmailWithUserId", mock.Anything, mock.Anything).Return(nil, nil) + usersClientCall := usersClient.On("SendEmail", mock.Anything, mock.Anything).Return(nil, nil) err := svc.SendInvitation(context.Background(), tc.session, tc.req) assert.True(t, errors.Contains(err, tc.err)) repoCall.Unset() diff --git a/groups/api/grpc/endpoint_test.go b/groups/api/grpc/endpoint_test.go index 7c2358a689..08b30eb68c 100644 --- a/groups/api/grpc/endpoint_test.go +++ b/groups/api/grpc/endpoint_test.go @@ -25,8 +25,6 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -const port = 0 - var ( validID = testsutil.GenerateUUID(&testing.T{}) valid = "valid" @@ -49,26 +47,26 @@ var ( ) func startGRPCServer(svc *prmocks.Service) (*grpc.Server, int) { - listener, err := net.Listen("tcp", ":0") - if err != nil { - panic(fmt.Sprintf("failed to obtain port: %s", err)) - } - server := grpc.NewServer() - grpcGroupsV1.RegisterGroupsServiceServer(server, grpcapi.NewServer(svc)) - go func() { - if err := server.Serve(listener); err != nil { - panic(fmt.Sprintf("failed to serve: %s", err)) - } - }() - p := listener.Addr().(*net.TCPAddr).Port - return server, p + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(fmt.Sprintf("failed to obtain port: %s", err)) + } + server := grpc.NewServer() + grpcGroupsV1.RegisterGroupsServiceServer(server, grpcapi.NewServer(svc)) + go func() { + if err := server.Serve(listener); err != nil { + panic(fmt.Sprintf("failed to serve: %s", err)) + } + }() + p := listener.Addr().(*net.TCPAddr).Port + return server, p } func TestRetrieveEntityEndpoint(t *testing.T) { - svc := new(prmocks.Service) - server, p := startGRPCServer(svc) - defer server.GracefulStop() - grpAddr := fmt.Sprintf("localhost:%d", p) + svc := new(prmocks.Service) + server, p := startGRPCServer(svc) + defer server.GracefulStop() + grpAddr := fmt.Sprintf("localhost:%d", p) conn, _ := grpc.NewClient(grpAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) diff --git a/internal/proto/emails/v1/emails.proto b/internal/proto/emails/v1/emails.proto new file mode 100644 index 0000000000..4945d9b14c --- /dev/null +++ b/internal/proto/emails/v1/emails.proto @@ -0,0 +1,34 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package emails.v1; +option go_package = "github.com/absmach/supermq/api/grpc/emails/v1"; + +// EmailService is a service that provides email-related functionalities +// for SuperMQ services. +service EmailService { + rpc SendEmail(EmailReq) returns (SendEmailRes) {} +} + +enum ContactType { + ID = 0; + Email = 1; +} + +message EmailReq { + string from = 1; // Sender + ContactType fromType = 2; // indicate sender id or email + repeated string tos = 3; // recipients + ContactType toType = 4; // indicate recipient id or email + string subject = 5; // Email subject + string content = 6; // Email content/URL + optional string template = 10; // the whole template to be parsed by emailer + optional string template_file = 11; // template file name to be parsed by emailer + map options = 12; // map that can be used in template +} + +message SendEmailRes { + string error = 1; +} diff --git a/internal/proto/users/v1/users.proto b/internal/proto/users/v1/users.proto deleted file mode 100644 index b1ace05e00..0000000000 --- a/internal/proto/users/v1/users.proto +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -syntax = "proto3"; - -package users.v1; -option go_package = "github.com/absmach/supermq/api/grpc/users/v1"; - -// UsersService is a service that provides user-related functionalities -// for SuperMQ services. -service UsersService { - rpc SendEmailWithUserId(SendEmailWithUserIdReq) returns (SendEmailRes) {} -} - -message SendEmailWithUserIdReq { - repeated string users = 1; // Email recipients - string from = 2; // Sender email address (optional) - string subject = 3; // Email subject - string header = 4; // Email header text - string user = 5; // User name - string content = 6; // Email content/URL - string footer = 7; // Email footer text -} - -message SendEmailRes { - bool sent = 1; // Whether the email was sent successfully -} diff --git a/pkg/grpcclient/client.go b/pkg/grpcclient/client.go index 435de5a1de..f8964f7c47 100644 --- a/pkg/grpcclient/client.go +++ b/pkg/grpcclient/client.go @@ -9,9 +9,9 @@ import ( grpcChannelsV1 "github.com/absmach/supermq/api/grpc/channels/v1" grpcClientsV1 "github.com/absmach/supermq/api/grpc/clients/v1" grpcDomainsV1 "github.com/absmach/supermq/api/grpc/domains/v1" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcGroupsV1 "github.com/absmach/supermq/api/grpc/groups/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" - grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" tokengrpc "github.com/absmach/supermq/auth/api/grpc/token" channelsgrpc "github.com/absmach/supermq/channels/api/grpc" clientsauth "github.com/absmach/supermq/clients/api/grpc" @@ -105,7 +105,7 @@ func SetupGroupsClient(ctx context.Context, cfg Config) (grpcGroupsV1.GroupsServ // For example: // // usersClient, usersHandler, err := grpcclient.SetupUsersClient(ctx, grpcclient.Config{}). -func SetupUsersClient(ctx context.Context, cfg Config) (grpcUsersV1.UsersServiceClient, Handler, error) { +func SetupUsersClient(ctx context.Context, cfg Config) (grpcEmailsV1.EmailServiceClient, Handler, error) { client, err := NewHandler(cfg) if err != nil { return nil, nil, err diff --git a/tools/config/.mockery.yaml b/tools/config/.mockery.yaml index 8315a80367..ff43d8a895 100644 --- a/tools/config/.mockery.yaml +++ b/tools/config/.mockery.yaml @@ -52,9 +52,9 @@ packages: dir: "./pkg/sdk/mocks" structname: "SDK" filename: "sdk.go" - github.com/absmach/supermq/api/grpc/users/v1: + github.com/absmach/supermq/api/grpc/emails/v1: interfaces: - UsersServiceClient: + EmailServiceClient: config: dir: "./users/mocks" github.com/absmach/supermq/auth: diff --git a/users/api/grpc/client.go b/users/api/grpc/client.go index 772b68ed36..8881366fb0 100644 --- a/users/api/grpc/client.go +++ b/users/api/grpc/client.go @@ -7,7 +7,7 @@ import ( "context" "time" - grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" + grpcUsersV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcapi "github.com/absmach/supermq/auth/api/grpc" "github.com/go-kit/kit/endpoint" kitgrpc "github.com/go-kit/kit/transport/grpc" @@ -16,7 +16,7 @@ import ( const usersSvcName = "users.v1.UsersService" -var _ grpcUsersV1.UsersServiceClient = (*usersGrpcClient)(nil) +var _ grpcUsersV1.EmailServiceClient = (*usersGrpcClient)(nil) type usersGrpcClient struct { sendEmail endpoint.Endpoint @@ -24,7 +24,7 @@ type usersGrpcClient struct { } // NewUsersClient returns new users gRPC client instance. -func NewUsersClient(conn *grpc.ClientConn, timeout time.Duration) grpcUsersV1.UsersServiceClient { +func NewUsersClient(conn *grpc.ClientConn, timeout time.Duration) grpcUsersV1.EmailServiceClient { return &usersGrpcClient{ sendEmail: kitgrpc.NewClient( conn, @@ -38,42 +38,52 @@ func NewUsersClient(conn *grpc.ClientConn, timeout time.Duration) grpcUsersV1.Us } } -func (client usersGrpcClient) SendEmailWithUserId(ctx context.Context, in *grpcUsersV1.SendEmailWithUserIdReq, opts ...grpc.CallOption) (*grpcUsersV1.SendEmailRes, error) { +func (client usersGrpcClient) SendEmail(ctx context.Context, in *grpcUsersV1.EmailReq, opts ...grpc.CallOption) (*grpcUsersV1.SendEmailRes, error) { ctx, cancel := context.WithTimeout(ctx, client.timeout) defer cancel() + options := in.GetOptions() res, err := client.sendEmail(ctx, sendEmailClientReq{ - to: in.GetUsers(), + to: in.GetTos(), from: in.GetFrom(), subject: in.GetSubject(), - header: in.GetHeader(), - user: in.GetUser(), + header: options["header"], + user: options["user"], content: in.GetContent(), - footer: in.GetFooter(), + footer: options["footer"], }) if err != nil { return &grpcUsersV1.SendEmailRes{}, grpcapi.DecodeError(err) } ser := res.(sendEmailClientRes) - return &grpcUsersV1.SendEmailRes{Sent: ser.sent}, nil + errMsg := "" + if !ser.sent { + errMsg = "failed to send email" + } + return &grpcUsersV1.SendEmailRes{Error: errMsg}, nil } func decodeSendEmailClientResponse(_ context.Context, grpcRes any) (any, error) { res := grpcRes.(*grpcUsersV1.SendEmailRes) - return sendEmailClientRes{sent: res.GetSent()}, nil + sent := res.GetError() == "" + return sendEmailClientRes{sent: sent}, nil } func encodeSendEmailClientRequest(_ context.Context, grpcReq any) (any, error) { req := grpcReq.(sendEmailClientReq) - return &grpcUsersV1.SendEmailWithUserIdReq{ - Users: req.to, - From: req.from, - Subject: req.subject, - Header: req.header, - User: req.user, - Content: req.content, - Footer: req.footer, + return &grpcUsersV1.EmailReq{ + Tos: req.to, + ToType: grpcUsersV1.ContactType_ID, + From: req.from, + FromType: grpcUsersV1.ContactType_ID, + Subject: req.subject, + Content: req.content, + Options: map[string]string{ + "header": req.header, + "user": req.user, + "footer": req.footer, + }, }, nil } diff --git a/users/api/grpc/server.go b/users/api/grpc/server.go index dd3ec0c72d..068dcb7026 100644 --- a/users/api/grpc/server.go +++ b/users/api/grpc/server.go @@ -6,21 +6,21 @@ package grpc import ( "context" - grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1" + grpcUsersV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcapi "github.com/absmach/supermq/auth/api/grpc" "github.com/absmach/supermq/users" kitgrpc "github.com/go-kit/kit/transport/grpc" ) -var _ grpcUsersV1.UsersServiceServer = (*usersGrpcServer)(nil) +var _ grpcUsersV1.EmailServiceServer = (*usersGrpcServer)(nil) type usersGrpcServer struct { - grpcUsersV1.UnimplementedUsersServiceServer + grpcUsersV1.UnimplementedEmailServiceServer sendEmail kitgrpc.Handler } // NewUsersServer creates a new users gRPC server. -func NewUsersServer(svc users.Service) grpcUsersV1.UsersServiceServer { +func NewUsersServer(svc users.Service) grpcUsersV1.EmailServiceServer { return &usersGrpcServer{ sendEmail: kitgrpc.NewServer( sendEmailEndpoint(svc), @@ -31,24 +31,29 @@ func NewUsersServer(svc users.Service) grpcUsersV1.UsersServiceServer { } func decodeSendEmailRequest(_ context.Context, grpcReq any) (any, error) { - req := grpcReq.(*grpcUsersV1.SendEmailWithUserIdReq) + req := grpcReq.(*grpcUsersV1.EmailReq) + opts := req.GetOptions() return sendEmailReq{ - to: req.GetUsers(), + to: req.GetTos(), from: req.GetFrom(), subject: req.GetSubject(), - header: req.GetHeader(), - user: req.GetUser(), + header: opts["header"], + user: opts["user"], content: req.GetContent(), - footer: req.GetFooter(), + footer: opts["footer"], }, nil } func encodeSendEmailResponse(_ context.Context, grpcRes any) (any, error) { res := grpcRes.(sendEmailRes) - return &grpcUsersV1.SendEmailRes{Sent: res.sent}, nil + errMsg := "" + if !res.sent { + errMsg = "failed to send email" + } + return &grpcUsersV1.SendEmailRes{Error: errMsg}, nil } -func (s *usersGrpcServer) SendEmail(ctx context.Context, req *grpcUsersV1.SendEmailWithUserIdReq) (*grpcUsersV1.SendEmailRes, error) { +func (s *usersGrpcServer) SendEmail(ctx context.Context, req *grpcUsersV1.EmailReq) (*grpcUsersV1.SendEmailRes, error) { _, res, err := s.sendEmail.ServeGRPC(ctx, req) if err != nil { return nil, grpcapi.EncodeError(err) diff --git a/users/mocks/email_service_client.go b/users/mocks/email_service_client.go new file mode 100644 index 0000000000..d582e18840 --- /dev/null +++ b/users/mocks/email_service_client.go @@ -0,0 +1,127 @@ +// Copyright (c) Abstract Machines + +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/absmach/supermq/api/grpc/emails/v1" + mock "github.com/stretchr/testify/mock" + "google.golang.org/grpc" +) + +// NewEmailServiceClient creates a new instance of EmailServiceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEmailServiceClient(t interface { + mock.TestingT + Cleanup(func()) +}) *EmailServiceClient { + mock := &EmailServiceClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// EmailServiceClient is an autogenerated mock type for the EmailServiceClient type +type EmailServiceClient struct { + mock.Mock +} + +type EmailServiceClient_Expecter struct { + mock *mock.Mock +} + +func (_m *EmailServiceClient) EXPECT() *EmailServiceClient_Expecter { + return &EmailServiceClient_Expecter{mock: &_m.Mock} +} + +// SendEmail provides a mock function for the type EmailServiceClient +func (_mock *EmailServiceClient) SendEmail(ctx context.Context, in *v1.EmailReq, opts ...grpc.CallOption) (*v1.SendEmailRes, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, in, opts) + } else { + tmpRet = _mock.Called(ctx, in) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for SendEmail") + } + + var r0 *v1.SendEmailRes + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.EmailReq, ...grpc.CallOption) (*v1.SendEmailRes, error)); ok { + return returnFunc(ctx, in, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.EmailReq, ...grpc.CallOption) *v1.SendEmailRes); ok { + r0 = returnFunc(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.SendEmailRes) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, *v1.EmailReq, ...grpc.CallOption) error); ok { + r1 = returnFunc(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// EmailServiceClient_SendEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmail' +type EmailServiceClient_SendEmail_Call struct { + *mock.Call +} + +// SendEmail is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.EmailReq +// - opts ...grpc.CallOption +func (_e *EmailServiceClient_Expecter) SendEmail(ctx interface{}, in interface{}, opts ...interface{}) *EmailServiceClient_SendEmail_Call { + return &EmailServiceClient_SendEmail_Call{Call: _e.mock.On("SendEmail", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *EmailServiceClient_SendEmail_Call) Run(run func(ctx context.Context, in *v1.EmailReq, opts ...grpc.CallOption)) *EmailServiceClient_SendEmail_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *v1.EmailReq + if args[1] != nil { + arg1 = args[1].(*v1.EmailReq) + } + var arg2 []grpc.CallOption + var variadicArgs []grpc.CallOption + if len(args) > 2 { + variadicArgs = args[2].([]grpc.CallOption) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *EmailServiceClient_SendEmail_Call) Return(sendEmailRes *v1.SendEmailRes, err error) *EmailServiceClient_SendEmail_Call { + _c.Call.Return(sendEmailRes, err) + return _c +} + +func (_c *EmailServiceClient_SendEmail_Call) RunAndReturn(run func(ctx context.Context, in *v1.EmailReq, opts ...grpc.CallOption) (*v1.SendEmailRes, error)) *EmailServiceClient_SendEmail_Call { + _c.Call.Return(run) + return _c +} diff --git a/users/mocks/users_service_client.go b/users/mocks/users_service_client.go deleted file mode 100644 index 3fe71665a2..0000000000 --- a/users/mocks/users_service_client.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Abstract Machines - -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/absmach/supermq/api/grpc/users/v1" - mock "github.com/stretchr/testify/mock" - "google.golang.org/grpc" -) - -// NewUsersServiceClient creates a new instance of UsersServiceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewUsersServiceClient(t interface { - mock.TestingT - Cleanup(func()) -}) *UsersServiceClient { - mock := &UsersServiceClient{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// UsersServiceClient is an autogenerated mock type for the UsersServiceClient type -type UsersServiceClient struct { - mock.Mock -} - -type UsersServiceClient_Expecter struct { - mock *mock.Mock -} - -func (_m *UsersServiceClient) EXPECT() *UsersServiceClient_Expecter { - return &UsersServiceClient_Expecter{mock: &_m.Mock} -} - -// SendEmailWithUserId provides a mock function for the type UsersServiceClient -func (_mock *UsersServiceClient) SendEmailWithUserId(ctx context.Context, in *v1.SendEmailWithUserIdReq, opts ...grpc.CallOption) (*v1.SendEmailRes, error) { - var tmpRet mock.Arguments - if len(opts) > 0 { - tmpRet = _mock.Called(ctx, in, opts) - } else { - tmpRet = _mock.Called(ctx, in) - } - ret := tmpRet - - if len(ret) == 0 { - panic("no return value specified for SendEmailWithUserId") - } - - var r0 *v1.SendEmailRes - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.SendEmailWithUserIdReq, ...grpc.CallOption) (*v1.SendEmailRes, error)); ok { - return returnFunc(ctx, in, opts...) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.SendEmailWithUserIdReq, ...grpc.CallOption) *v1.SendEmailRes); ok { - r0 = returnFunc(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.SendEmailRes) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, *v1.SendEmailWithUserIdReq, ...grpc.CallOption) error); ok { - r1 = returnFunc(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// UsersServiceClient_SendEmailWithUserId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmailWithUserId' -type UsersServiceClient_SendEmailWithUserId_Call struct { - *mock.Call -} - -// SendEmailWithUserId is a helper method to define mock.On call -// - ctx context.Context -// - in *v1.SendEmailWithUserIdReq -// - opts ...grpc.CallOption -func (_e *UsersServiceClient_Expecter) SendEmailWithUserId(ctx interface{}, in interface{}, opts ...interface{}) *UsersServiceClient_SendEmailWithUserId_Call { - return &UsersServiceClient_SendEmailWithUserId_Call{Call: _e.mock.On("SendEmailWithUserId", - append([]interface{}{ctx, in}, opts...)...)} -} - -func (_c *UsersServiceClient_SendEmailWithUserId_Call) Run(run func(ctx context.Context, in *v1.SendEmailWithUserIdReq, opts ...grpc.CallOption)) *UsersServiceClient_SendEmailWithUserId_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 *v1.SendEmailWithUserIdReq - if args[1] != nil { - arg1 = args[1].(*v1.SendEmailWithUserIdReq) - } - var arg2 []grpc.CallOption - var variadicArgs []grpc.CallOption - if len(args) > 2 { - variadicArgs = args[2].([]grpc.CallOption) - } - arg2 = variadicArgs - run( - arg0, - arg1, - arg2..., - ) - }) - return _c -} - -func (_c *UsersServiceClient_SendEmailWithUserId_Call) Return(sendEmailRes *v1.SendEmailRes, err error) *UsersServiceClient_SendEmailWithUserId_Call { - _c.Call.Return(sendEmailRes, err) - return _c -} - -func (_c *UsersServiceClient_SendEmailWithUserId_Call) RunAndReturn(run func(ctx context.Context, in *v1.SendEmailWithUserIdReq, opts ...grpc.CallOption) (*v1.SendEmailRes, error)) *UsersServiceClient_SendEmailWithUserId_Call { - _c.Call.Return(run) - return _c -} From c450dc3899913a4609bafe80277c8b7ddf363cad Mon Sep 17 00:00:00 2001 From: WashingtonKK Date: Mon, 3 Nov 2025 12:00:50 +0300 Subject: [PATCH 4/9] Add email template validation and fix proto linting Signed-off-by: WashingtonKK --- api/grpc/emails/v1/emails.pb.go | 52 ++++++++++++++------------- domains/service.go | 4 +-- internal/proto/emails/v1/emails.proto | 17 ++++----- users/api/grpc/client.go | 4 +-- users/api/grpc/endpoint.go | 31 ++++++++++++---- users/api/grpc/server.go | 28 +++++++++++---- 6 files changed, 86 insertions(+), 50 deletions(-) diff --git a/api/grpc/emails/v1/emails.pb.go b/api/grpc/emails/v1/emails.pb.go index e9d0876696..6ab02213a1 100644 --- a/api/grpc/emails/v1/emails.pb.go +++ b/api/grpc/emails/v1/emails.pb.go @@ -27,19 +27,22 @@ const ( type ContactType int32 const ( - ContactType_ID ContactType = 0 - ContactType_Email ContactType = 1 + ContactType_CONTACT_TYPE_UNSPECIFIED ContactType = 0 + ContactType_CONTACT_TYPE_ID ContactType = 1 + ContactType_CONTACT_TYPE_EMAIL ContactType = 2 ) // Enum value maps for ContactType. var ( ContactType_name = map[int32]string{ - 0: "ID", - 1: "Email", + 0: "CONTACT_TYPE_UNSPECIFIED", + 1: "CONTACT_TYPE_ID", + 2: "CONTACT_TYPE_EMAIL", } ContactType_value = map[string]int32{ - "ID": 0, - "Email": 1, + "CONTACT_TYPE_UNSPECIFIED": 0, + "CONTACT_TYPE_ID": 1, + "CONTACT_TYPE_EMAIL": 2, } ) @@ -73,14 +76,14 @@ func (ContactType) EnumDescriptor() ([]byte, []int) { type EmailReq struct { state protoimpl.MessageState `protogen:"open.v1"` From string `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` // Sender - FromType ContactType `protobuf:"varint,2,opt,name=fromType,proto3,enum=emails.v1.ContactType" json:"fromType,omitempty"` // indicate sender id or email + FromType ContactType `protobuf:"varint,2,opt,name=from_type,json=fromType,proto3,enum=emails.v1.ContactType" json:"from_type,omitempty"` // indicate sender id or email Tos []string `protobuf:"bytes,3,rep,name=tos,proto3" json:"tos,omitempty"` // recipients - ToType ContactType `protobuf:"varint,4,opt,name=toType,proto3,enum=emails.v1.ContactType" json:"toType,omitempty"` // indicate recipient id or email + ToType ContactType `protobuf:"varint,4,opt,name=to_type,json=toType,proto3,enum=emails.v1.ContactType" json:"to_type,omitempty"` // indicate recipient id or email Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"` // Email subject Content string `protobuf:"bytes,6,opt,name=content,proto3" json:"content,omitempty"` // Email content/URL - Template *string `protobuf:"bytes,10,opt,name=template,proto3,oneof" json:"template,omitempty"` // the whole template to be parsed by emailer - TemplateFile *string `protobuf:"bytes,11,opt,name=template_file,json=templateFile,proto3,oneof" json:"template_file,omitempty"` // template file name to be parsed by emailer - Options map[string]string `protobuf:"bytes,12,rep,name=options,proto3" json:"options,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // map that can be used in template + Template *string `protobuf:"bytes,10,opt,name=template,proto3,oneof" json:"template,omitempty"` // whole template for emailer + TemplateFile *string `protobuf:"bytes,11,opt,name=template_file,json=templateFile,proto3,oneof" json:"template_file,omitempty"` // template file for emailer + Options map[string]string `protobuf:"bytes,12,rep,name=options,proto3" json:"options,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // map for template unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -126,7 +129,7 @@ func (x *EmailReq) GetFromType() ContactType { if x != nil { return x.FromType } - return ContactType_ID + return ContactType_CONTACT_TYPE_UNSPECIFIED } func (x *EmailReq) GetTos() []string { @@ -140,7 +143,7 @@ func (x *EmailReq) GetToType() ContactType { if x != nil { return x.ToType } - return ContactType_ID + return ContactType_CONTACT_TYPE_UNSPECIFIED } func (x *EmailReq) GetSubject() string { @@ -226,12 +229,12 @@ var File_emails_v1_emails_proto protoreflect.FileDescriptor const file_emails_v1_emails_proto_rawDesc = "" + "\n" + - "\x16emails/v1/emails.proto\x12\temails.v1\"\xaa\x03\n" + + "\x16emails/v1/emails.proto\x12\temails.v1\"\xac\x03\n" + "\bEmailReq\x12\x12\n" + - "\x04from\x18\x01 \x01(\tR\x04from\x122\n" + - "\bfromType\x18\x02 \x01(\x0e2\x16.emails.v1.ContactTypeR\bfromType\x12\x10\n" + - "\x03tos\x18\x03 \x03(\tR\x03tos\x12.\n" + - "\x06toType\x18\x04 \x01(\x0e2\x16.emails.v1.ContactTypeR\x06toType\x12\x18\n" + + "\x04from\x18\x01 \x01(\tR\x04from\x123\n" + + "\tfrom_type\x18\x02 \x01(\x0e2\x16.emails.v1.ContactTypeR\bfromType\x12\x10\n" + + "\x03tos\x18\x03 \x03(\tR\x03tos\x12/\n" + + "\ato_type\x18\x04 \x01(\x0e2\x16.emails.v1.ContactTypeR\x06toType\x12\x18\n" + "\asubject\x18\x05 \x01(\tR\asubject\x12\x18\n" + "\acontent\x18\x06 \x01(\tR\acontent\x12\x1f\n" + "\btemplate\x18\n" + @@ -244,10 +247,11 @@ const file_emails_v1_emails_proto_rawDesc = "" + "\t_templateB\x10\n" + "\x0e_template_file\"$\n" + "\fSendEmailRes\x12\x14\n" + - "\x05error\x18\x01 \x01(\tR\x05error* \n" + - "\vContactType\x12\x06\n" + - "\x02ID\x10\x00\x12\t\n" + - "\x05Email\x10\x012K\n" + + "\x05error\x18\x01 \x01(\tR\x05error*X\n" + + "\vContactType\x12\x1c\n" + + "\x18CONTACT_TYPE_UNSPECIFIED\x10\x00\x12\x13\n" + + "\x0fCONTACT_TYPE_ID\x10\x01\x12\x16\n" + + "\x12CONTACT_TYPE_EMAIL\x10\x022K\n" + "\fEmailService\x12;\n" + "\tSendEmail\x12\x13.emails.v1.EmailReq\x1a\x17.emails.v1.SendEmailRes\"\x00B/Z-github.com/absmach/supermq/api/grpc/emails/v1b\x06proto3" @@ -272,8 +276,8 @@ var file_emails_v1_emails_proto_goTypes = []any{ nil, // 3: emails.v1.EmailReq.OptionsEntry } var file_emails_v1_emails_proto_depIdxs = []int32{ - 0, // 0: emails.v1.EmailReq.fromType:type_name -> emails.v1.ContactType - 0, // 1: emails.v1.EmailReq.toType:type_name -> emails.v1.ContactType + 0, // 0: emails.v1.EmailReq.from_type:type_name -> emails.v1.ContactType + 0, // 1: emails.v1.EmailReq.to_type:type_name -> emails.v1.ContactType 3, // 2: emails.v1.EmailReq.options:type_name -> emails.v1.EmailReq.OptionsEntry 1, // 3: emails.v1.EmailService.SendEmail:input_type -> emails.v1.EmailReq 2, // 4: emails.v1.EmailService.SendEmail:output_type -> emails.v1.SendEmailRes diff --git a/domains/service.go b/domains/service.go index e4b4999859..6019ea2fd7 100644 --- a/domains/service.go +++ b/domains/service.go @@ -211,10 +211,10 @@ func (svc *service) SendInvitation(ctx context.Context, session authn.Session, i if _, err := svc.usersClient.SendEmail(ctx, &grpcEmailsV1.EmailReq{ Tos: []string{invitation.InviteeUserID}, - ToType: grpcEmailsV1.ContactType_ID, + ToType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, Subject: "Invitation to join Domain", From: invitation.InvitedBy, - FromType: grpcEmailsV1.ContactType_ID, + FromType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, }); err != nil { return err } diff --git a/internal/proto/emails/v1/emails.proto b/internal/proto/emails/v1/emails.proto index 4945d9b14c..9829b62690 100644 --- a/internal/proto/emails/v1/emails.proto +++ b/internal/proto/emails/v1/emails.proto @@ -13,20 +13,21 @@ service EmailService { } enum ContactType { - ID = 0; - Email = 1; + CONTACT_TYPE_UNSPECIFIED = 0; + CONTACT_TYPE_ID = 1; + CONTACT_TYPE_EMAIL = 2; } message EmailReq { string from = 1; // Sender - ContactType fromType = 2; // indicate sender id or email + ContactType from_type = 2; // indicate sender id or email repeated string tos = 3; // recipients - ContactType toType = 4; // indicate recipient id or email + ContactType to_type = 4; // indicate recipient id or email string subject = 5; // Email subject - string content = 6; // Email content/URL - optional string template = 10; // the whole template to be parsed by emailer - optional string template_file = 11; // template file name to be parsed by emailer - map options = 12; // map that can be used in template + string content = 6; // Email content/URL + optional string template = 10; // whole template for emailer + optional string template_file = 11; // template file for emailer + map options = 12; // map for template } message SendEmailRes { diff --git a/users/api/grpc/client.go b/users/api/grpc/client.go index 8881366fb0..53ed45cc7f 100644 --- a/users/api/grpc/client.go +++ b/users/api/grpc/client.go @@ -74,9 +74,9 @@ func encodeSendEmailClientRequest(_ context.Context, grpcReq any) (any, error) { req := grpcReq.(sendEmailClientReq) return &grpcUsersV1.EmailReq{ Tos: req.to, - ToType: grpcUsersV1.ContactType_ID, + ToType: grpcUsersV1.ContactType_CONTACT_TYPE_ID, From: req.from, - FromType: grpcUsersV1.ContactType_ID, + FromType: grpcUsersV1.ContactType_CONTACT_TYPE_ID, Subject: req.subject, Content: req.content, Options: map[string]string{ diff --git a/users/api/grpc/endpoint.go b/users/api/grpc/endpoint.go index 3d292ce1ff..5f21018a3a 100644 --- a/users/api/grpc/endpoint.go +++ b/users/api/grpc/endpoint.go @@ -4,7 +4,9 @@ package grpc import ( + "bytes" "context" + "text/template" "github.com/absmach/supermq/pkg/errors" "github.com/absmach/supermq/users" @@ -27,13 +29,16 @@ func sendEmailEndpoint(svc users.Service) endpoint.Endpoint { } type sendEmailReq struct { - to []string - from string - subject string - header string - user string - content string - footer string + to []string + from string + subject string + header string + user string + content string + footer string + Template string + templateFile string + Options map[string]string } func (req sendEmailReq) validate() error { @@ -43,6 +48,18 @@ func (req sendEmailReq) validate() error { if req.subject == "" { return errors.ErrMalformedEntity } + + if req.Template != "" { + t, err := template.New("body").Parse(req.Template) + if err != nil { + return err + } + buff := new(bytes.Buffer) + if err := t.Execute(buff, req.Options); err != nil { + return err + } + } + return nil } diff --git a/users/api/grpc/server.go b/users/api/grpc/server.go index 068dcb7026..ac678f5125 100644 --- a/users/api/grpc/server.go +++ b/users/api/grpc/server.go @@ -33,14 +33,28 @@ func NewUsersServer(svc users.Service) grpcUsersV1.EmailServiceServer { func decodeSendEmailRequest(_ context.Context, grpcReq any) (any, error) { req := grpcReq.(*grpcUsersV1.EmailReq) opts := req.GetOptions() + + tmpl := "" + if req.Template != nil { + tmpl = *req.Template + } + + templateFile := "" + if req.TemplateFile != nil { + templateFile = *req.TemplateFile + } + return sendEmailReq{ - to: req.GetTos(), - from: req.GetFrom(), - subject: req.GetSubject(), - header: opts["header"], - user: opts["user"], - content: req.GetContent(), - footer: opts["footer"], + to: req.GetTos(), + from: req.GetFrom(), + subject: req.GetSubject(), + header: opts["header"], + user: opts["user"], + content: req.GetContent(), + footer: opts["footer"], + Template: tmpl, + templateFile: templateFile, + Options: opts, }, nil } From 345f308c111132477eff55ecc7a55ce4e03795ed Mon Sep 17 00:00:00 2001 From: WashingtonKK Date: Mon, 3 Nov 2025 13:03:54 +0300 Subject: [PATCH 5/9] Fix typo in email template env variable Signed-off-by: WashingtonKK --- docker/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/.env b/docker/.env index 7e3f85076c..255b64dfb8 100644 --- a/docker/.env +++ b/docker/.env @@ -277,7 +277,7 @@ SMQ_PASSWORD_RESET_URL_PREFIX=http://localhost/password-reset SMQ_PASSWORD_RESET_EMAIL_TEMPLATE=reset-password-email.tmpl SMQ_VERIFICATION_URL_PREFIX=http://localhost/verify-email SMQ_VERIFICATION_EMAIL_TEMPLATE=verification-email.tmpl -SM_EMAIL_TEMPLATE=invitation.tmpl +SMQ_EMAIL_TEMPLATE=invitation.tmpl #### Users Client Config SMQ_USERS_URL=users:9002 From 730b3c1b8eae5972bd276be3a87255c62e3c1367 Mon Sep 17 00:00:00 2001 From: WashingtonKK Date: Tue, 4 Nov 2025 18:34:01 +0300 Subject: [PATCH 6/9] update email subject Signed-off-by: WashingtonKK --- domains/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domains/service.go b/domains/service.go index 6019ea2fd7..593318f851 100644 --- a/domains/service.go +++ b/domains/service.go @@ -212,7 +212,7 @@ func (svc *service) SendInvitation(ctx context.Context, session authn.Session, i if _, err := svc.usersClient.SendEmail(ctx, &grpcEmailsV1.EmailReq{ Tos: []string{invitation.InviteeUserID}, ToType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, - Subject: "Invitation to join Domain", + Subject: "Invitation", From: invitation.InvitedBy, FromType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, }); err != nil { From a7c82f74e49a92d09c6c9cfc13eaa72078a2445d Mon Sep 17 00:00:00 2001 From: WashingtonKK Date: Wed, 5 Nov 2025 00:59:58 +0300 Subject: [PATCH 7/9] return error from service Signed-off-by: WashingtonKK --- users/api/grpc/endpoint.go | 13 ++++++++++--- users/api/grpc/server.go | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/users/api/grpc/endpoint.go b/users/api/grpc/endpoint.go index 5f21018a3a..629cf94e29 100644 --- a/users/api/grpc/endpoint.go +++ b/users/api/grpc/endpoint.go @@ -17,14 +17,20 @@ func sendEmailEndpoint(svc users.Service) endpoint.Endpoint { return func(ctx context.Context, request any) (any, error) { req := request.(sendEmailReq) if err := req.validate(); err != nil { - return sendEmailRes{}, err + return sendEmailRes{ + err: err, + }, err } if err := svc.SendEmailWithUserId(ctx, req.to, req.from, req.subject, req.header, req.user, req.content, req.footer); err != nil { - return sendEmailRes{}, err + return sendEmailRes{ + err: err, + }, err } - return sendEmailRes{sent: true}, nil + return sendEmailRes{ + sent: true, + }, nil } } @@ -65,4 +71,5 @@ func (req sendEmailReq) validate() error { type sendEmailRes struct { sent bool + err error } diff --git a/users/api/grpc/server.go b/users/api/grpc/server.go index ac678f5125..acf8fe29a6 100644 --- a/users/api/grpc/server.go +++ b/users/api/grpc/server.go @@ -61,8 +61,8 @@ func decodeSendEmailRequest(_ context.Context, grpcReq any) (any, error) { func encodeSendEmailResponse(_ context.Context, grpcRes any) (any, error) { res := grpcRes.(sendEmailRes) errMsg := "" - if !res.sent { - errMsg = "failed to send email" + if !res.sent && res.err != nil { + errMsg = res.err.Error() } return &grpcUsersV1.SendEmailRes{Error: errMsg}, nil } From 067c6fc836303207d83d8da4233d8534a4f42b54 Mon Sep 17 00:00:00 2001 From: WashingtonKK Date: Wed, 5 Nov 2025 16:28:07 +0300 Subject: [PATCH 8/9] Add contact type support to SendEmail method Signed-off-by: WashingtonKK --- users/api/grpc/endpoint.go | 5 +- users/api/grpc/server.go | 2 + users/events/streams.go | 5 +- users/middleware/authorization.go | 5 +- users/middleware/logging.go | 10 +- users/middleware/metrics.go | 7 +- users/middleware/tracing.go | 10 +- users/mocks/service.go | 49 +++++--- users/service.go | 41 +++++-- users/service_test.go | 188 ++++++++++++++++++++++++++++++ users/users.go | 6 +- 11 files changed, 283 insertions(+), 45 deletions(-) diff --git a/users/api/grpc/endpoint.go b/users/api/grpc/endpoint.go index 629cf94e29..7f73eb845c 100644 --- a/users/api/grpc/endpoint.go +++ b/users/api/grpc/endpoint.go @@ -8,6 +8,7 @@ import ( "context" "text/template" + grpcUsersV1 "github.com/absmach/supermq/api/grpc/emails/v1" "github.com/absmach/supermq/pkg/errors" "github.com/absmach/supermq/users" "github.com/go-kit/kit/endpoint" @@ -22,7 +23,7 @@ func sendEmailEndpoint(svc users.Service) endpoint.Endpoint { }, err } - if err := svc.SendEmailWithUserId(ctx, req.to, req.from, req.subject, req.header, req.user, req.content, req.footer); err != nil { + if err := svc.SendEmail(ctx, req.to, req.toType, req.from, req.fromType, req.subject, req.header, req.user, req.content, req.footer); err != nil { return sendEmailRes{ err: err, }, err @@ -36,7 +37,9 @@ func sendEmailEndpoint(svc users.Service) endpoint.Endpoint { type sendEmailReq struct { to []string + toType grpcUsersV1.ContactType from string + fromType grpcUsersV1.ContactType subject string header string user string diff --git a/users/api/grpc/server.go b/users/api/grpc/server.go index acf8fe29a6..8859a60763 100644 --- a/users/api/grpc/server.go +++ b/users/api/grpc/server.go @@ -46,7 +46,9 @@ func decodeSendEmailRequest(_ context.Context, grpcReq any) (any, error) { return sendEmailReq{ to: req.GetTos(), + toType: req.GetToType(), from: req.GetFrom(), + fromType: req.GetFromType(), subject: req.GetSubject(), header: opts["header"], user: opts["user"], diff --git a/users/events/streams.go b/users/events/streams.go index 54db13faef..80350d03f2 100644 --- a/users/events/streams.go +++ b/users/events/streams.go @@ -6,6 +6,7 @@ package events import ( "context" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/pkg/events" @@ -445,6 +446,6 @@ func (es *eventStore) OAuthAddUserPolicy(ctx context.Context, user users.User) e return es.Publish(ctx, addPolicyStream, event) } -func (es *eventStore) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { - return es.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) +func (es *eventStore) SendEmail(ctx context.Context, to []string, toType grpcEmailsV1.ContactType, from string, fromType grpcEmailsV1.ContactType, subject, header, user, content, footer string) error { + return es.svc.SendEmail(ctx, to, toType, from, fromType, subject, header, user, content, footer) } diff --git a/users/middleware/authorization.go b/users/middleware/authorization.go index e1925529bf..33a24f9dd1 100644 --- a/users/middleware/authorization.go +++ b/users/middleware/authorization.go @@ -6,6 +6,7 @@ package middleware import ( "context" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" smqauth "github.com/absmach/supermq/auth" "github.com/absmach/supermq/pkg/authn" @@ -335,8 +336,8 @@ func (am *authorizationMiddleware) OAuthAddUserPolicy(ctx context.Context, user return am.svc.OAuthAddUserPolicy(ctx, user) } -func (am *authorizationMiddleware) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { - return am.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) +func (am *authorizationMiddleware) SendEmail(ctx context.Context, to []string, toType grpcEmailsV1.ContactType, from string, fromType grpcEmailsV1.ContactType, subject, header, user, content, footer string) error { + return am.svc.SendEmail(ctx, to, toType, from, fromType, subject, header, user, content, footer) } func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error { diff --git a/users/middleware/logging.go b/users/middleware/logging.go index 485c172cd7..7d37af5b5b 100644 --- a/users/middleware/logging.go +++ b/users/middleware/logging.go @@ -8,6 +8,7 @@ import ( "log/slog" "time" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/users" @@ -522,13 +523,16 @@ func (lm *loggingMiddleware) OAuthAddUserPolicy(ctx context.Context, user users. return lm.svc.OAuthAddUserPolicy(ctx, user) } -// SendEmailWithUserId logs the send_email request. It logs the recipients and the time it took to complete the request. -func (lm *loggingMiddleware) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) (err error) { +// SendEmail logs the send_email request. It logs the recipients and the time it took to complete the request. +func (lm *loggingMiddleware) SendEmail(ctx context.Context, to []string, toType grpcEmailsV1.ContactType, from string, fromType grpcEmailsV1.ContactType, subject, header, user, content, footer string) (err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), slog.String("request_id", middleware.GetReqID(ctx)), slog.Any("to", to), + slog.String("to_type", toType.String()), + slog.String("from", from), + slog.String("from_type", fromType.String()), slog.String("subject", subject), } if err != nil { @@ -538,5 +542,5 @@ func (lm *loggingMiddleware) SendEmailWithUserId(ctx context.Context, to []strin } lm.logger.Info("Send email completed successfully", args...) }(time.Now()) - return lm.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) + return lm.svc.SendEmail(ctx, to, toType, from, fromType, subject, header, user, content, footer) } diff --git a/users/middleware/metrics.go b/users/middleware/metrics.go index 869d0d365d..91438fd5f6 100644 --- a/users/middleware/metrics.go +++ b/users/middleware/metrics.go @@ -7,6 +7,7 @@ import ( "context" "time" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/users" @@ -246,11 +247,11 @@ func (ms *metricsMiddleware) OAuthAddUserPolicy(ctx context.Context, user users. return ms.svc.OAuthAddUserPolicy(ctx, user) } -// SendEmailWithUserId instruments SendEmail method with metrics. -func (ms *metricsMiddleware) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { +// SendEmail instruments SendEmail method with metrics. +func (ms *metricsMiddleware) SendEmail(ctx context.Context, to []string, toType grpcEmailsV1.ContactType, from string, fromType grpcEmailsV1.ContactType, subject, header, user, content, footer string) error { defer func(begin time.Time) { ms.counter.With("method", "send_email").Add(1) ms.latency.With("method", "send_email").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) + return ms.svc.SendEmail(ctx, to, toType, from, fromType, subject, header, user, content, footer) } diff --git a/users/middleware/tracing.go b/users/middleware/tracing.go index a9b22a36e8..bab9bebe26 100644 --- a/users/middleware/tracing.go +++ b/users/middleware/tracing.go @@ -6,6 +6,7 @@ package middleware import ( "context" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/pkg/tracing" @@ -249,13 +250,16 @@ func (tm *tracingMiddleware) OAuthAddUserPolicy(ctx context.Context, user users. return tm.svc.OAuthAddUserPolicy(ctx, user) } -// SendEmailWithUserId traces the "SendEmailWithUserId" operation of the wrapped users.Service. -func (tm *tracingMiddleware) SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error { +// SendEmail traces the "SendEmail" operation of the wrapped users.Service. +func (tm *tracingMiddleware) SendEmail(ctx context.Context, to []string, toType grpcEmailsV1.ContactType, from string, fromType grpcEmailsV1.ContactType, subject, header, user, content, footer string) error { ctx, span := tracing.StartSpan(ctx, tm.tracer, "svc_send_email", trace.WithAttributes( attribute.StringSlice("to", to), + attribute.String("to_type", toType.String()), + attribute.String("from", from), + attribute.String("from_type", fromType.String()), attribute.String("subject", subject), )) defer span.End() - return tm.svc.SendEmailWithUserId(ctx, to, from, subject, header, user, content, footer) + return tm.svc.SendEmail(ctx, to, toType, from, fromType, subject, header, user, content, footer) } diff --git a/users/mocks/service.go b/users/mocks/service.go index af0db00514..f92329dd25 100644 --- a/users/mocks/service.go +++ b/users/mocks/service.go @@ -11,6 +11,7 @@ package mocks import ( "context" + v10 "github.com/absmach/supermq/api/grpc/emails/v1" "github.com/absmach/supermq/api/grpc/token/v1" "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/users" @@ -867,42 +868,44 @@ func (_c *Service_SearchUsers_Call) RunAndReturn(run func(ctx context.Context, p return _c } -// SendEmailWithUserId provides a mock function for the type Service -func (_mock *Service) SendEmailWithUserId(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string) error { - ret := _mock.Called(ctx, to, from, subject, header, user, content, footer) +// SendEmail provides a mock function for the type Service +func (_mock *Service) SendEmail(ctx context.Context, to []string, toType v10.ContactType, from string, fromType v10.ContactType, subject string, header string, user string, content string, footer string) error { + ret := _mock.Called(ctx, to, toType, from, fromType, subject, header, user, content, footer) if len(ret) == 0 { - panic("no return value specified for SendEmailWithUserId") + panic("no return value specified for SendEmail") } var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, []string, string, string, string, string, string, string) error); ok { - r0 = returnFunc(ctx, to, from, subject, header, user, content, footer) + if returnFunc, ok := ret.Get(0).(func(context.Context, []string, v10.ContactType, string, v10.ContactType, string, string, string, string, string) error); ok { + r0 = returnFunc(ctx, to, toType, from, fromType, subject, header, user, content, footer) } else { r0 = ret.Error(0) } return r0 } -// Service_SendEmailWithUserId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmailWithUserId' -type Service_SendEmailWithUserId_Call struct { +// Service_SendEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendEmail' +type Service_SendEmail_Call struct { *mock.Call } -// SendEmailWithUserId is a helper method to define mock.On call +// SendEmail is a helper method to define mock.On call // - ctx context.Context // - to []string +// - toType v10.ContactType // - from string +// - fromType v10.ContactType // - subject string // - header string // - user string // - content string // - footer string -func (_e *Service_Expecter) SendEmailWithUserId(ctx interface{}, to interface{}, from interface{}, subject interface{}, header interface{}, user interface{}, content interface{}, footer interface{}) *Service_SendEmailWithUserId_Call { - return &Service_SendEmailWithUserId_Call{Call: _e.mock.On("SendEmailWithUserId", ctx, to, from, subject, header, user, content, footer)} +func (_e *Service_Expecter) SendEmail(ctx interface{}, to interface{}, toType interface{}, from interface{}, fromType interface{}, subject interface{}, header interface{}, user interface{}, content interface{}, footer interface{}) *Service_SendEmail_Call { + return &Service_SendEmail_Call{Call: _e.mock.On("SendEmail", ctx, to, toType, from, fromType, subject, header, user, content, footer)} } -func (_c *Service_SendEmailWithUserId_Call) Run(run func(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string)) *Service_SendEmailWithUserId_Call { +func (_c *Service_SendEmail_Call) Run(run func(ctx context.Context, to []string, toType v10.ContactType, from string, fromType v10.ContactType, subject string, header string, user string, content string, footer string)) *Service_SendEmail_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -912,17 +915,17 @@ func (_c *Service_SendEmailWithUserId_Call) Run(run func(ctx context.Context, to if args[1] != nil { arg1 = args[1].([]string) } - var arg2 string + var arg2 v10.ContactType if args[2] != nil { - arg2 = args[2].(string) + arg2 = args[2].(v10.ContactType) } var arg3 string if args[3] != nil { arg3 = args[3].(string) } - var arg4 string + var arg4 v10.ContactType if args[4] != nil { - arg4 = args[4].(string) + arg4 = args[4].(v10.ContactType) } var arg5 string if args[5] != nil { @@ -936,6 +939,14 @@ func (_c *Service_SendEmailWithUserId_Call) Run(run func(ctx context.Context, to if args[7] != nil { arg7 = args[7].(string) } + var arg8 string + if args[8] != nil { + arg8 = args[8].(string) + } + var arg9 string + if args[9] != nil { + arg9 = args[9].(string) + } run( arg0, arg1, @@ -945,17 +956,19 @@ func (_c *Service_SendEmailWithUserId_Call) Run(run func(ctx context.Context, to arg5, arg6, arg7, + arg8, + arg9, ) }) return _c } -func (_c *Service_SendEmailWithUserId_Call) Return(err error) *Service_SendEmailWithUserId_Call { +func (_c *Service_SendEmail_Call) Return(err error) *Service_SendEmail_Call { _c.Call.Return(err) return _c } -func (_c *Service_SendEmailWithUserId_Call) RunAndReturn(run func(ctx context.Context, to []string, from string, subject string, header string, user string, content string, footer string) error) *Service_SendEmailWithUserId_Call { +func (_c *Service_SendEmail_Call) RunAndReturn(run func(ctx context.Context, to []string, toType v10.ContactType, from string, fromType v10.ContactType, subject string, header string, user string, content string, footer string) error) *Service_SendEmail_Call { _c.Call.Return(run) return _c } diff --git a/users/service.go b/users/service.go index 5a5f044b8d..f707e1e121 100644 --- a/users/service.go +++ b/users/service.go @@ -15,6 +15,7 @@ import ( "time" "github.com/absmach/supermq" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" apiutil "github.com/absmach/supermq/api/http/util" smqauth "github.com/absmach/supermq/auth" @@ -815,20 +816,38 @@ func changed(updated *string, old string) bool { return *updated != old } -func (svc service) SendEmailWithUserId(ctx context.Context, userIds []string, from, subject, header, user, content, footer string) error { - for i, userId := range userIds { - u, err := svc.users.RetrieveByID(ctx, userId) - if err != nil { - return errors.Wrap(svcerr.ErrViewEntity, err) +func (svc service) SendEmail(ctx context.Context, to []string, toType grpcEmailsV1.ContactType, from string, fromType grpcEmailsV1.ContactType, subject, header, user, content, footer string) error { + // Convert recipients based on contact type + emails := make([]string, len(to)) + for i, contact := range to { + switch toType { + case grpcEmailsV1.ContactType_CONTACT_TYPE_ID: + u, err := svc.users.RetrieveByID(ctx, contact) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + emails[i] = u.Email + case grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL: + emails[i] = contact + default: + return errors.Wrap(svcerr.ErrMalformedEntity, errors.New("invalid contact type for recipients")) } - - userIds[i] = u.Email } - inviter, err := svc.users.RetrieveByID(ctx, from) - if err != nil { - return err + // Convert sender based on contact type + var senderName string + switch fromType { + case grpcEmailsV1.ContactType_CONTACT_TYPE_ID: + inviter, err := svc.users.RetrieveByID(ctx, from) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + senderName = inviter.FirstName + " " + inviter.LastName + case grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL: + senderName = from + default: + return errors.Wrap(svcerr.ErrMalformedEntity, errors.New("invalid contact type for sender")) } - return svc.email.SendCustom(userIds, inviter.FirstName+" "+inviter.LastName, subject, header, user, content, footer) + return svc.email.SendCustom(emails, senderName, subject, header, user, content, footer) } diff --git a/users/service_test.go b/users/service_test.go index 2dfd230e72..3702e5c1f3 100644 --- a/users/service_test.go +++ b/users/service_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" smqauth "github.com/absmach/supermq/auth" authmocks "github.com/absmach/supermq/auth/mocks" @@ -2066,3 +2067,190 @@ func TestVerifyEmail(t *testing.T) { }) } } + +func TestSendEmail(t *testing.T) { + svc, _, cRepo, _, e := newService() + + recipientID := "d4ebb847-5d0e-4e46-bdd9-b6aceaaa3a22" + senderID := "e5fcc958-6e1f-5f57-cee0-c7bdfbbb4a33" + recipientEmail := "recipient@example.com" + senderEmail := "sender@example.com" + subject := "Test Subject" + header := "Test Header" + userField := "Test User" + content := "Test Content" + footer := "Test Footer" + + recipientUser := users.User{ + ID: recipientID, + Email: recipientEmail, + FirstName: "Recipient", + LastName: "User", + } + + senderUser := users.User{ + ID: senderID, + Email: senderEmail, + FirstName: "Sender", + LastName: "User", + } + + cases := []struct { + desc string + to []string + toType grpcEmailsV1.ContactType + from string + fromType grpcEmailsV1.ContactType + retrieveRecipientID string + retrieveRecipient users.User + retrieveRecipientErr error + retrieveSenderID string + retrieveSender users.User + retrieveSenderErr error + expectedEmails []string + expectedSenderName string + sendCustomErr error + err error + }{ + { + desc: "send email with ID contact types successfully", + to: []string{recipientID}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + from: senderID, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + retrieveRecipientID: recipientID, + retrieveRecipient: recipientUser, + retrieveSenderID: senderID, + retrieveSender: senderUser, + expectedEmails: []string{recipientEmail}, + expectedSenderName: "Sender User", + err: nil, + }, + { + desc: "send email with EMAIL contact types successfully", + to: []string{recipientEmail}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + from: senderEmail, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + expectedEmails: []string{recipientEmail}, + expectedSenderName: senderEmail, + err: nil, + }, + { + desc: "send email with mixed contact types (to: ID, from: EMAIL)", + to: []string{recipientID}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + from: senderEmail, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + retrieveRecipientID: recipientID, + retrieveRecipient: recipientUser, + expectedEmails: []string{recipientEmail}, + expectedSenderName: senderEmail, + err: nil, + }, + { + desc: "send email with mixed contact types (to: EMAIL, from: ID)", + to: []string{recipientEmail}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + from: senderID, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + retrieveSenderID: senderID, + retrieveSender: senderUser, + expectedEmails: []string{recipientEmail}, + expectedSenderName: "Sender User", + err: nil, + }, + { + desc: "send email fails when recipient ID not found", + to: []string{recipientID}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + from: senderEmail, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + retrieveRecipientID: recipientID, + retrieveRecipientErr: repoerr.ErrNotFound, + err: svcerr.ErrViewEntity, + }, + { + desc: "send email fails when sender ID not found", + to: []string{recipientEmail}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + from: senderID, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + retrieveSenderID: senderID, + retrieveSenderErr: repoerr.ErrNotFound, + err: svcerr.ErrViewEntity, + }, + { + desc: "send email fails with invalid recipient contact type", + to: []string{recipientID}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_UNSPECIFIED, + from: senderEmail, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + expectedEmails: []string{}, + expectedSenderName: "", + err: svcerr.ErrMalformedEntity, + }, + { + desc: "send email fails with invalid sender contact type", + to: []string{recipientEmail}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + from: senderID, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_UNSPECIFIED, + err: svcerr.ErrMalformedEntity, + }, + { + desc: "send email with multiple recipients using IDs", + to: []string{recipientID, recipientID}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + from: senderEmail, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + retrieveRecipientID: recipientID, + retrieveRecipient: recipientUser, + expectedEmails: []string{recipientEmail, recipientEmail}, + expectedSenderName: senderEmail, + err: nil, + }, + { + desc: "send email fails when email sending fails", + to: []string{recipientEmail}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + from: senderEmail, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + expectedEmails: []string{recipientEmail}, + expectedSenderName: senderEmail, + sendCustomErr: errors.New("email sending failed"), + err: errors.New("email sending failed"), + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + var repoCall, repoCall1 *mock.Call + + if tc.retrieveRecipientID != "" { + repoCall = cRepo.On("RetrieveByID", context.Background(), tc.retrieveRecipientID).Return(tc.retrieveRecipient, tc.retrieveRecipientErr) + } + if tc.retrieveSenderID != "" { + repoCall1 = cRepo.On("RetrieveByID", context.Background(), tc.retrieveSenderID).Return(tc.retrieveSender, tc.retrieveSenderErr) + } + + if tc.err == nil || (tc.retrieveRecipientErr == nil && tc.retrieveSenderErr == nil) { + if len(tc.expectedEmails) > 0 { + e.On("SendCustom", tc.expectedEmails, tc.expectedSenderName, subject, header, userField, content, footer).Return(tc.sendCustomErr) + } + } + + err := svc.SendEmail(context.Background(), tc.to, tc.toType, tc.from, tc.fromType, subject, header, userField, content, footer) + + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + + if repoCall != nil { + repoCall.Unset() + } + if repoCall1 != nil { + repoCall1.Unset() + } + e.Mock = mock.Mock{} + }) + } +} diff --git a/users/users.go b/users/users.go index 21952e3227..07591408d6 100644 --- a/users/users.go +++ b/users/users.go @@ -8,6 +8,7 @@ import ( "net/mail" "time" + grpcEmailsV1 "github.com/absmach/supermq/api/grpc/emails/v1" grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1" "github.com/absmach/supermq/pkg/authn" "github.com/absmach/supermq/pkg/errors" @@ -244,6 +245,7 @@ type Service interface { // OAuthAddUserPolicy adds a policy to the user for an OAuth request. OAuthAddUserPolicy(ctx context.Context, user User) error - // SendEmailWithUserId sends an email using the email agent. - SendEmailWithUserId(ctx context.Context, to []string, from, subject, header, user, content, footer string) error + // SendEmail sends an email using the email agent. + // fromType and toType indicate whether from and to are IDs or emails (ContactType enum). + SendEmail(ctx context.Context, to []string, toType grpcEmailsV1.ContactType, from string, fromType grpcEmailsV1.ContactType, subject, header, user, content, footer string) error } From d11206486ccd10c8e5909e5074c18e0893bedd23 Mon Sep 17 00:00:00 2001 From: WashingtonKK Date: Wed, 5 Nov 2025 19:51:44 +0300 Subject: [PATCH 9/9] lint Signed-off-by: WashingtonKK --- users/service_test.go | 86 +++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/users/service_test.go b/users/service_test.go index 3702e5c1f3..89989d4494 100644 --- a/users/service_test.go +++ b/users/service_test.go @@ -2096,35 +2096,35 @@ func TestSendEmail(t *testing.T) { } cases := []struct { - desc string - to []string - toType grpcEmailsV1.ContactType - from string - fromType grpcEmailsV1.ContactType - retrieveRecipientID string - retrieveRecipient users.User + desc string + to []string + toType grpcEmailsV1.ContactType + from string + fromType grpcEmailsV1.ContactType + retrieveRecipientID string + retrieveRecipient users.User retrieveRecipientErr error - retrieveSenderID string - retrieveSender users.User - retrieveSenderErr error - expectedEmails []string - expectedSenderName string - sendCustomErr error - err error + retrieveSenderID string + retrieveSender users.User + retrieveSenderErr error + expectedEmails []string + expectedSenderName string + sendCustomErr error + err error }{ { - desc: "send email with ID contact types successfully", - to: []string{recipientID}, - toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, - from: senderID, - fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + desc: "send email with ID contact types successfully", + to: []string{recipientID}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + from: senderID, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, retrieveRecipientID: recipientID, - retrieveRecipient: recipientUser, - retrieveSenderID: senderID, - retrieveSender: senderUser, - expectedEmails: []string{recipientEmail}, - expectedSenderName: "Sender User", - err: nil, + retrieveRecipient: recipientUser, + retrieveSenderID: senderID, + retrieveSender: senderUser, + expectedEmails: []string{recipientEmail}, + expectedSenderName: "Sender User", + err: nil, }, { desc: "send email with EMAIL contact types successfully", @@ -2137,16 +2137,16 @@ func TestSendEmail(t *testing.T) { err: nil, }, { - desc: "send email with mixed contact types (to: ID, from: EMAIL)", - to: []string{recipientID}, - toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, - from: senderEmail, - fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + desc: "send email with mixed contact types (to: ID, from: EMAIL)", + to: []string{recipientID}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + from: senderEmail, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, retrieveRecipientID: recipientID, - retrieveRecipient: recipientUser, - expectedEmails: []string{recipientEmail}, - expectedSenderName: senderEmail, - err: nil, + retrieveRecipient: recipientUser, + expectedEmails: []string{recipientEmail}, + expectedSenderName: senderEmail, + err: nil, }, { desc: "send email with mixed contact types (to: EMAIL, from: ID)", @@ -2199,16 +2199,16 @@ func TestSendEmail(t *testing.T) { err: svcerr.ErrMalformedEntity, }, { - desc: "send email with multiple recipients using IDs", - to: []string{recipientID, recipientID}, - toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, - from: senderEmail, - fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, + desc: "send email with multiple recipients using IDs", + to: []string{recipientID, recipientID}, + toType: grpcEmailsV1.ContactType_CONTACT_TYPE_ID, + from: senderEmail, + fromType: grpcEmailsV1.ContactType_CONTACT_TYPE_EMAIL, retrieveRecipientID: recipientID, - retrieveRecipient: recipientUser, - expectedEmails: []string{recipientEmail, recipientEmail}, - expectedSenderName: senderEmail, - err: nil, + retrieveRecipient: recipientUser, + expectedEmails: []string{recipientEmail, recipientEmail}, + expectedSenderName: senderEmail, + err: nil, }, { desc: "send email fails when email sending fails",