diff --git a/services/auth/internal/network/grpc/server.go b/services/auth/internal/network/grpc/server.go index 48df97e..6d51039 100644 --- a/services/auth/internal/network/grpc/server.go +++ b/services/auth/internal/network/grpc/server.go @@ -49,10 +49,10 @@ func (s *grpcServer) TokenMetadata(ctx context.Context, token *contracts.TokenCo return nil, err } - _, err = s.cache.GetUserId(accessDetails) + userId, err := s.cache.GetUserId(accessDetails) if err != nil { return nil, err } - return &contracts.TokenMetadataResponse{IsValid: true}, nil + return &contracts.TokenMetadataResponse{IsValid: true, Id: userId}, nil } diff --git a/services/user/.env.example b/services/user/.env.example index 12588f4..53fc6e6 100644 --- a/services/user/.env.example +++ b/services/user/.env.example @@ -12,3 +12,5 @@ USER_DATABASE_SCHEMA= USER_REST_URL= USER_GRPC_URL= + +AUTH_GRPC_URL= diff --git a/services/user/cmd/server/main.go b/services/user/cmd/server/main.go index 244aab4..00c1e5b 100644 --- a/services/user/cmd/server/main.go +++ b/services/user/cmd/server/main.go @@ -6,7 +6,8 @@ import ( "github.com/mohammadne/bookman/user/config" "github.com/mohammadne/bookman/user/internal/database" "github.com/mohammadne/bookman/user/internal/network" - grpc_server "github.com/mohammadne/bookman/user/internal/network/grpc/server" + "github.com/mohammadne/bookman/user/internal/network/grpc" + grpc_client "github.com/mohammadne/bookman/user/internal/network/grpc/clients" "github.com/mohammadne/bookman/user/internal/network/rest" "github.com/mohammadne/bookman/user/pkg/logger" "github.com/spf13/cobra" @@ -52,10 +53,13 @@ func main(environment config.Environment) { db := database.NewMysqlDatabase(cfg.Database, log) + authGrpc := grpc_client.NewUser(cfg.GrpcAuth, log) + authGrpc.Setup() + // serving application servers servers := []network.Server{ - rest.New(cfg.Rest, log, db), - grpc_server.New(cfg.Grpc, log, db), + rest.New(cfg.Rest, log, db, authGrpc), + grpc.NewServer(cfg.GrpcServer, log, db), } for _, server := range servers { diff --git a/services/user/config/config.go b/services/user/config/config.go index 7b946a4..fb5ad63 100644 --- a/services/user/config/config.go +++ b/services/user/config/config.go @@ -2,14 +2,15 @@ package config import ( "github.com/mohammadne/bookman/user/internal/database" - grpc_server "github.com/mohammadne/bookman/user/internal/network/grpc/server" + "github.com/mohammadne/bookman/user/internal/network/grpc" "github.com/mohammadne/bookman/user/internal/network/rest" "github.com/mohammadne/bookman/user/pkg/logger" ) type Config struct { - Logger *logger.Config - Database *database.Config - Rest *rest.Config - Grpc *grpc_server.Config + Logger *logger.Config + Database *database.Config + Rest *rest.Config + GrpcServer *grpc.Config + GrpcAuth *grpc.Config } diff --git a/services/user/config/load.go b/services/user/config/load.go index 4d20000..998f308 100644 --- a/services/user/config/load.go +++ b/services/user/config/load.go @@ -4,7 +4,7 @@ import ( "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" "github.com/mohammadne/bookman/user/internal/database" - grpc_server "github.com/mohammadne/bookman/user/internal/network/grpc/server" + "github.com/mohammadne/bookman/user/internal/network/grpc" "github.com/mohammadne/bookman/user/internal/network/rest" "github.com/mohammadne/bookman/user/pkg/logger" ) @@ -23,14 +23,16 @@ func Load(env Environment) *Config { cfg.Logger = &logger.Config{} cfg.Database = &database.Config{} cfg.Rest = &rest.Config{} - cfg.Grpc = &grpc_server.Config{} + cfg.GrpcServer = &grpc.Config{} + cfg.GrpcAuth = &grpc.Config{} // process envconfig.MustProcess("user", cfg) envconfig.MustProcess("user_logger", cfg.Logger) envconfig.MustProcess("user_database", cfg.Database) envconfig.MustProcess("user_rest", cfg.Rest) - envconfig.MustProcess("user_grpc", cfg.Grpc) + envconfig.MustProcess("user_grpc", cfg.GrpcServer) + envconfig.MustProcess("auth_grpc", cfg.GrpcAuth) return cfg } diff --git a/services/user/internal/models/user.go b/services/user/internal/models/user.go index 3243457..6a8e633 100644 --- a/services/user/internal/models/user.go +++ b/services/user/internal/models/user.go @@ -1,7 +1,5 @@ package models -import "encoding/json" - type User struct { Id uint64 `json:"id"` FirstName string `json:"first_name"` @@ -11,13 +9,12 @@ type User struct { DateCreated string `json:"date_created,omitempty"` } -func (user *User) Marshall(isPublic bool) interface{} { +func (user *User) Marshall(isPublic bool) *User { user.Password = "" if isPublic { user.Email = "" user.DateCreated = "" } - userJson, _ := json.Marshal(user) - return userJson + return user } diff --git a/services/user/internal/network/grpc/client/auth.go b/services/user/internal/network/grpc/client/auth.go deleted file mode 100644 index 91968c5..0000000 --- a/services/user/internal/network/grpc/client/auth.go +++ /dev/null @@ -1,34 +0,0 @@ -package grpc_client - -import ( - "context" - "errors" - - "github.com/mohammadne/bookman/user/internal/models" - "github.com/mohammadne/bookman/user/internal/network/grpc/contracts" - "github.com/mohammadne/go-pkgs/logger" -) - -var ( - errInvalidToken = errors.New("token is invalid") -) - -type AuthClient interface { - GetTokenMetadata(string) (*models.User, error) -} - -func (g *grpcClient) GetTokenMetadata(token string) (*models.User, error) { - contract := &contracts.TokenContract{Token: token} - response, err := g.authClient.TokenMetadata(context.Background(), contract) - if err != nil { - g.logger.Error("grpc get token metadata", logger.Error(err)) - return nil, err - } - - if response.IsValid { - g.logger.Error("not valid token metadata", logger.Error(err)) - return nil, errInvalidToken - } - - return &models.User{Id: response.Id}, nil -} diff --git a/services/user/internal/network/grpc/client/client.go b/services/user/internal/network/grpc/client/client.go deleted file mode 100644 index 0a151b1..0000000 --- a/services/user/internal/network/grpc/client/client.go +++ /dev/null @@ -1,35 +0,0 @@ -package grpc_client - -import ( - "github.com/mohammadne/bookman/user/internal/network/grpc/contracts" - "github.com/mohammadne/go-pkgs/logger" - "google.golang.org/grpc" -) - -type Client interface { - AuthClient -} - -type grpcClient struct { - config *Config - logger logger.Logger - - authClient contracts.AuthClient -} - -func New(cfg *Config, log logger.Logger) Client { - return &grpcClient{config: cfg, logger: log} -} - -func (g *grpcClient) Setup() { - authConnection, err := grpc.Dial(g.config.AuthAddress, grpc.WithInsecure()) - if err != nil { - g.logger.Fatal( - "error getting auth grpc connection", - logger.String("address", g.config.AuthAddress), - logger.Error(err), - ) - } - - g.authClient = contracts.NewAuthClient(authConnection) -} diff --git a/services/user/internal/network/grpc/client/config.go b/services/user/internal/network/grpc/client/config.go deleted file mode 100644 index 50d1814..0000000 --- a/services/user/internal/network/grpc/client/config.go +++ /dev/null @@ -1,5 +0,0 @@ -package grpc_client - -type Config struct { - AuthAddress string `split_words:"true"` -} diff --git a/services/user/internal/network/grpc/clients/auth.go b/services/user/internal/network/grpc/clients/auth.go new file mode 100644 index 0000000..812e9ce --- /dev/null +++ b/services/user/internal/network/grpc/clients/auth.go @@ -0,0 +1,61 @@ +package grpc_clients + +import ( + context "context" + + "github.com/mohammadne/bookman/user/internal/network/grpc/contracts" + + "github.com/mohammadne/bookman/user/internal/network/grpc" + "github.com/mohammadne/go-pkgs/failures" + "github.com/mohammadne/go-pkgs/logger" + grpcPkg "google.golang.org/grpc" +) + +type Auth interface { + GetTokenMetadata(string) (uint64, failures.Failure) +} + +type authClient struct { + logger logger.Logger + config *grpc.Config + + client contracts.AuthClient +} + +func NewUser(cfg *grpc.Config, log logger.Logger) *authClient { + return &authClient{config: cfg, logger: log} +} + +func (g *authClient) Setup() { + authConnection, err := grpcPkg.Dial(g.config.Url, grpcPkg.WithInsecure()) + if err != nil { + g.logger.Fatal( + "error getting auth grpc connection", + logger.String("address", g.config.Url), + logger.Error(err), + ) + } + + g.client = contracts.NewAuthClient(authConnection) +} + +var ( + errGettingToken = failures.Rest{}.NewInternalServer("error getting token metadata") + invalidToken = failures.Rest{}.NewInternalServer("invalid token") +) + +func (g *authClient) GetTokenMetadata(token string) (uint64, failures.Failure) { + contract := &contracts.TokenContract{Token: token} + response, err := g.client.TokenMetadata(context.Background(), contract) + if err != nil { + g.logger.Error("grpc get token metadata", logger.Error(err)) + return 0, errGettingToken + } + + if !response.IsValid { + g.logger.Error("not valid token metadata", logger.Error(err)) + return 0, invalidToken + } + + return response.Id, nil +} diff --git a/services/user/internal/network/grpc/config.go b/services/user/internal/network/grpc/config.go new file mode 100644 index 0000000..5fc5862 --- /dev/null +++ b/services/user/internal/network/grpc/config.go @@ -0,0 +1,5 @@ +package grpc + +type Config struct { + Url string +} diff --git a/services/user/internal/network/grpc/server/server.go b/services/user/internal/network/grpc/server.go similarity index 91% rename from services/user/internal/network/grpc/server/server.go rename to services/user/internal/network/grpc/server.go index 572a7fb..90cbfb6 100644 --- a/services/user/internal/network/grpc/server/server.go +++ b/services/user/internal/network/grpc/server.go @@ -9,7 +9,7 @@ import ( "github.com/mohammadne/bookman/user/internal/network/grpc/contracts" "github.com/mohammadne/go-pkgs/failures" "github.com/mohammadne/go-pkgs/logger" - grpc "google.golang.org/grpc" + "google.golang.org/grpc" ) type grpcServer struct { @@ -23,7 +23,7 @@ type grpcServer struct { contracts.UnimplementedUserServer } -func New(cfg *Config, log logger.Logger, db database.Database) *grpcServer { +func NewServer(cfg *Config, log logger.Logger, db database.Database) *grpcServer { s := &grpcServer{config: cfg, logger: log, database: db} s.server = grpc.NewServer() @@ -33,7 +33,7 @@ func New(cfg *Config, log logger.Logger, db database.Database) *grpcServer { } func (s *grpcServer) Serve(<-chan struct{}) { - listener, err := net.Listen("tcp", s.config.URL) + listener, err := net.Listen("tcp", s.config.Url) if err != nil { panic(err) } diff --git a/services/user/internal/network/grpc/server/config.go b/services/user/internal/network/grpc/server/config.go deleted file mode 100644 index f34f2cf..0000000 --- a/services/user/internal/network/grpc/server/config.go +++ /dev/null @@ -1,6 +0,0 @@ -package grpc - -// Config is web configuration -type Config struct { - URL string `split_words:"true"` -} diff --git a/services/user/internal/network/rest/handlers.go b/services/user/internal/network/rest/handlers.go index 681da47..e6fae70 100644 --- a/services/user/internal/network/rest/handlers.go +++ b/services/user/internal/network/rest/handlers.go @@ -5,34 +5,49 @@ import ( "strconv" "github.com/labstack/echo/v4" + "github.com/mohammadne/bookman/user/internal/models" + "github.com/mohammadne/go-pkgs/failures" + "github.com/mohammadne/go-pkgs/logger" ) // get is responsible to provide HTTP Get Location functionality func (rest *restEcho) getUser(ctx echo.Context) error { - idStr := ctx.Param("id") - if idStr == "" { - rest.logger.Error("user id is nil") - return ctx.String(http.StatusBadRequest, "bad request") + user, failure := rest.getUserByIdString(ctx.Param("id")) + if failure != nil { + return ctx.JSON(failure.Status(), failure) } - id, parseErr := strconv.ParseInt(idStr, 10, 64) - if parseErr != nil { - rest.logger.Error("user id is malformed") - return ctx.String(http.StatusBadRequest, "bad request") - } + return ctx.JSON(http.StatusOK, user.Marshall(true)) +} - user, readErr := rest.database.FindUserById(id) - if readErr != nil { - return ctx.JSON(readErr.Status(), readErr) +func (rest *restEcho) getMyUser(ctx echo.Context) error { + user, failure := rest.getUserByIdString(ctx.Get("token_user_id").(string)) + if failure != nil { + rest.logger.Error(failure.Message(), logger.Error(failure)) + return ctx.JSON(failure.Status(), failure) } return ctx.JSON(http.StatusOK, user.Marshall(false)) } -func (rest *restEcho) getMyUser(ctx echo.Context) error { +func (rest *restEcho) searchUsers(ctx echo.Context) error { return nil } -func (rest *restEcho) searchUsers(ctx echo.Context) error { - return nil +func (rest *restEcho) getUserByIdString(idStr string) (*models.User, failures.Failure) { + if idStr == "" { + return nil, failures.Rest{}.NewBadRequest("invalid user id is given") + } + + id, parseErr := strconv.ParseInt(idStr, 10, 64) + if parseErr != nil { + return nil, failures.Rest{}.NewBadRequest("given user id is malformed") + } + + user, failure := rest.database.FindUserById(id) + if failure != nil { + return nil, failure + } + + return user, nil } diff --git a/services/user/internal/network/rest/middlewares.go b/services/user/internal/network/rest/middlewares.go new file mode 100644 index 0000000..24cfdbf --- /dev/null +++ b/services/user/internal/network/rest/middlewares.go @@ -0,0 +1,49 @@ +package rest + +import ( + "net/http" + "strconv" + "strings" + + "github.com/labstack/echo/v4" + "github.com/mohammadne/go-pkgs/failures" + "github.com/mohammadne/go-pkgs/logger" +) + +func (rest *restEcho) authenticate(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + token, failure := extractToken(ctx.Request()) + if failure != nil { + rest.logger.Error("invalid token", logger.Error(failure)) + return ctx.JSON(failure.Status(), failure) + } + + userId, failure := rest.authGrpc.GetTokenMetadata(token) + if failure != nil || userId == 0 { + return ctx.JSON(failure.Status(), failure) + } + + ctx.Set("token_user_id", strconv.FormatUint(userId, 10)) + return next(ctx) + } +} + +var ( + missingAuth = failures.Rest{}.NewUnauthorized("authorization header is missing") + invalidAuth = failures.Rest{}.NewUnauthorized("authorization header is malformed") +) + +func extractToken(r *http.Request) (string, failures.Failure) { + bearToken := r.Header.Get("Authorization") + + if bearToken == "" { + return "", missingAuth + } + + strArr := strings.Split(bearToken, " ") + if len(strArr) != 2 { + return "", invalidAuth + } + + return strArr[1], nil +} diff --git a/services/user/internal/network/rest/server.go b/services/user/internal/network/rest/server.go index 8866364..1392a9f 100644 --- a/services/user/internal/network/rest/server.go +++ b/services/user/internal/network/rest/server.go @@ -3,6 +3,7 @@ package rest import ( "github.com/labstack/echo/v4" "github.com/mohammadne/bookman/user/internal/database" + grpc_client "github.com/mohammadne/bookman/user/internal/network/grpc/clients" "github.com/mohammadne/go-pkgs/logger" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -12,13 +13,14 @@ type restEcho struct { config *Config logger logger.Logger database database.Database + authGrpc grpc_client.Auth // internal dependencies instance *echo.Echo } -func New(cfg *Config, log logger.Logger, db database.Database) *restEcho { - handler := &restEcho{config: cfg, logger: log, database: db} +func New(cfg *Config, log logger.Logger, db database.Database, ag grpc_client.Auth) *restEcho { + handler := &restEcho{config: cfg, logger: log, database: db, authGrpc: ag} handler.instance = echo.New() handler.instance.HideBanner = true @@ -31,9 +33,9 @@ func (rest *restEcho) setupRoutes() { authGroup := rest.instance.Group("/users") authGroup.POST("/metrics", echo.WrapHandler(promhttp.Handler())) - authGroup.GET("/:id", rest.getUser) - authGroup.GET("/me", rest.getMyUser) - authGroup.GET("/search", rest.searchUsers) + authGroup.GET("/:id", rest.getUser, rest.authenticate) + authGroup.GET("/me", rest.getMyUser, rest.authenticate) + authGroup.GET("/search", rest.searchUsers, rest.authenticate) } func (rest *restEcho) Serve(<-chan struct{}) {