diff --git a/cmd/backend/config_defaults/config.yaml b/cmd/backend/config_defaults/config.yaml index d5df5f4..f2e060c 100644 --- a/cmd/backend/config_defaults/config.yaml +++ b/cmd/backend/config_defaults/config.yaml @@ -1,5 +1,5 @@ port: 8080 postgres_url: "postgres://postgres:postgres@localhost:5432/postgres" -jwt_signing_key: "./jwt_signing_key" +jwt_signing_key: "./config_defaults/jwt_signing_key" kafka_url: "localhost:9092" kafka_topic: "backend_events" \ No newline at end of file diff --git a/cmd/backend/server/handlers/user_create_handler.go b/cmd/backend/server/handlers/user_create_handler.go index 09610e0..70d4a30 100644 --- a/cmd/backend/server/handlers/user_create_handler.go +++ b/cmd/backend/server/handlers/user_create_handler.go @@ -2,8 +2,9 @@ package handlers import ( "backend/internal/core/services" + httpserver "backend/internal/http_server" "backend/pkg/logger" - "encoding/json" + "context" "github.com/gin-gonic/gin" ) @@ -20,54 +21,27 @@ type createUserOutput struct { Name string `json:"name"` } -func NewUserCreateHandler(logger logger.Logger, userService services.UserService) gin.HandlerFunc { - return func(c *gin.Context) { - ctxLogger := logger.WithContext(c) +func NewUserCreateHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { + return httpserver.WrapGin(log, + func(ctx context.Context, input createUserInput) (createUserOutput, error) { + user, err := userService.CreateUser(ctx, + services.UserCreateParams{ + Email: input.Email, + Password: input.Password, + Name: input.Name, + }, + ) - params := createUserInput{} - if err := c.ShouldBindJSON(¶ms); err != nil { - ctxLogger.Error().Err(err).Msg("bad input body model") - c.Data(400, "plain/text", []byte(err.Error())) - return - } + out := createUserOutput{} + if err != nil { + return out, err + } - dto, err := userService.CreateUser( - c, - services.UserCreateParams{ - Email: params.Email, - Password: params.Password, - Name: params.Name, - }, - ) - if err == services.ErrUserExists { - ctxLogger.Error().Err(err).Msg("user already exists") - c.Data(400, "plain/text", []byte(err.Error())) - return - } - if err == services.ErrUserBadPassword { - ctxLogger.Error().Err(err).Msg("password does not satisfy requirements") - c.Data(400, "plain/text", []byte(err.Error())) - return - } - if err != nil { - ctxLogger.Error().Err(err).Msg("unexpected create user error") - c.Data(500, "plain/text", []byte(err.Error())) - return - } - - resultBody, err := json.Marshal( - createUserOutput{ - Id: dto.Id, - Email: dto.Email, - Name: dto.Name, - }, - ) - if err != nil { - ctxLogger.Error().Err(err).Msg("marshal user model error") - c.Data(500, "plain/text", []byte(err.Error())) - return - } - - c.Data(200, "application/json", resultBody) - } + return createUserOutput{ + Id: user.Id, + Email: user.Email, + Name: user.Name, + }, nil + }, + ) } diff --git a/cmd/backend/server/handlers/user_login_handler.go b/cmd/backend/server/handlers/user_login_handler.go index d1878d4..72aa789 100644 --- a/cmd/backend/server/handlers/user_login_handler.go +++ b/cmd/backend/server/handlers/user_login_handler.go @@ -2,8 +2,9 @@ package handlers import ( "backend/internal/core/services" + httpserver "backend/internal/http_server" "backend/pkg/logger" - "encoding/json" + "context" "github.com/gin-gonic/gin" ) @@ -17,43 +18,17 @@ type loginUserOutput struct { Token string `json:"token"` } -func NewUserLoginHandler(logger logger.Logger, userService services.UserService) gin.HandlerFunc { - return func(c *gin.Context) { - ctxLogger := logger.WithContext(c).WithPrefix("NewUserLoginHandler") +func NewUserLoginHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc { + return httpserver.WrapGin(log, + func(ctx context.Context, input loginUserInput) (loginUserOutput, error) { + token, err := userService.AuthenticateUser(ctx, input.Login, input.Password) + if err != nil { + return loginUserOutput{}, err + } - params := loginUserInput{} - if err := c.ShouldBindJSON(¶ms); err != nil { - ctxLogger.Error().Err(err).Msg("bad input body model") - c.AbortWithError(400, err) - return - } - - token, err := userService.AuthenticateUser(c, params.Login, params.Password) - if err == services.ErrUserNotExists { - ctxLogger.Error().Err(err).Msg("user does not exist") - c.AbortWithError(400, err) - return - } - if err == services.ErrUserWrongPassword { - ctxLogger.Error().Err(err).Msg("wrong password") - c.AbortWithError(400, err) - return - } - if err != nil { - ctxLogger.Error().Err(err).Msg("AuthenticateUser internal error") - c.AbortWithError(500, err) - return - } - - resultBody, err := json.Marshal(loginUserOutput{ - Token: token, - }) - if err != nil { - ctxLogger.Error().Err(err).Msg("marshal json internal error") - c.AbortWithError(500, err) - return - } - - c.Data(200, "application/json", resultBody) - } + return loginUserOutput{ + Token: token, + }, nil + }, + ) } diff --git a/cmd/shortlinks/grpc.go b/cmd/shortlinks/grpc.go new file mode 100644 index 0000000..c35bf71 --- /dev/null +++ b/cmd/shortlinks/grpc.go @@ -0,0 +1,31 @@ +package main + +import ( + "backend/internal/core/services" + "backend/internal/grpc_server/shortlinks" + httpserver "backend/internal/http_server" + "backend/pkg/logger" + "context" +) + +func NewShortlinksGrpc(log logger.Logger, shortlinkService services.ShortlinkService, host string) *ShortlinksGrpc { + return &ShortlinksGrpc{ + handler: NewCreateHandler(log, shortlinkService, host), + } +} + +type ShortlinksGrpc struct { + shortlinks.UnimplementedShortlinksServer + handler httpserver.Handler[shortlinkCreateInput, shortlinkCreateOutput] +} + +func (s *ShortlinksGrpc) Create(ctx context.Context, req *shortlinks.CreateRequest) (*shortlinks.CreateResponse, error) { + output, err := s.handler(ctx, shortlinkCreateInput{req.Url}) + if err != nil { + return nil, err + } + + return &shortlinks.CreateResponse{ + Link: output.Link, + }, nil +} diff --git a/cmd/shortlinks/handlers.go b/cmd/shortlinks/handlers.go index 3fdc089..0af19af 100644 --- a/cmd/shortlinks/handlers.go +++ b/cmd/shortlinks/handlers.go @@ -2,93 +2,52 @@ package main import ( "backend/internal/core/services" - "backend/internal/grpc_server/shortlinks" + httpserver "backend/internal/http_server" "backend/pkg/logger" "context" - "encoding/json" "fmt" "net/url" "github.com/gin-gonic/gin" ) +type shortlinkCreateInput struct { + Url string `json:"url"` +} + type shortlinkCreateOutput struct { Link string `json:"link"` } -type ShortlinksGrpc struct { - shortlinks.UnimplementedShortlinksServer - log logger.Logger - host string - shortlinkService services.ShortlinkService -} +func NewCreateHandler( + log logger.Logger, + shortlinkService services.ShortlinkService, + host string, +) httpserver.Handler[shortlinkCreateInput, shortlinkCreateOutput] { + return func(ctx context.Context, input shortlinkCreateInput) (shortlinkCreateOutput, error) { + output := shortlinkCreateOutput{} -func (s *ShortlinksGrpc) Create(ctx context.Context, req *shortlinks.CreateRequest) (*shortlinks.CreateResponse, error) { - ctxLogger := s.log.WithContext(ctx) - - rawUrl := req.GetUrl() - if rawUrl == "" { - ctxLogger.Error().Msg("url query param missing") - return nil, fmt.Errorf("url query param missing") - } - - u, err := url.Parse(rawUrl) - if err != nil { - ctxLogger.Error().Err(err).Msg("error parsing url param") - return nil, err - } - u.Scheme = "https" - - linkId, err := s.shortlinkService.CreateShortlink(ctx, u.String()) - if err != nil { - ctxLogger.Error().Err(err).Msg("err creating shortlink") - return nil, err - } - - return &shortlinks.CreateResponse{ - Link: fmt.Sprintf("%s/s/%s", s.host, linkId), - }, nil -} - -func NewShortlinkCreateHandler(logger logger.Logger, shortlinkService services.ShortlinkService, host string) gin.HandlerFunc { - return func(ctx *gin.Context) { - ctxLogger := logger.WithContext(ctx) - - rawUrl := ctx.Query("url") - if rawUrl == "" { - ctxLogger.Error().Msg("url query param missing") - ctx.AbortWithError(400, fmt.Errorf("url query param missing")) - return - } - - u, err := url.Parse(rawUrl) + u, err := url.Parse(input.Url) if err != nil { - ctxLogger.Error().Err(err).Msg("error parsing url param") - ctx.Data(400, "plain/text", []byte(err.Error())) - return + return output, err } u.Scheme = "https" linkId, err := shortlinkService.CreateShortlink(ctx, u.String()) if err != nil { - ctxLogger.Error().Err(err).Msg("err creating shortlink") - ctx.Data(500, "plain/text", []byte(err.Error())) - return + return output, err } - resultBody, err := json.Marshal(shortlinkCreateOutput{ + return shortlinkCreateOutput{ Link: fmt.Sprintf("%s/s/%s", host, linkId), - }) - if err != nil { - ctxLogger.Error().Err(err).Msg("err marshalling shortlink") - ctx.AbortWithError(500, err) - return - } - - ctx.Data(200, "application/json", resultBody) + }, nil } } +func NewShortlinkCreateHandler(log logger.Logger, shortlinkService services.ShortlinkService, host string) gin.HandlerFunc { + return httpserver.WrapGin(log, NewCreateHandler(log, shortlinkService, host)) +} + func NewShortlinkResolveHandler(logger logger.Logger, shortlinkService services.ShortlinkService) gin.HandlerFunc { return func(ctx *gin.Context) { ctxLogger := logger.WithContext(ctx) diff --git a/cmd/shortlinks/main.go b/cmd/shortlinks/main.go index d6ef4ed..4fb9324 100644 --- a/cmd/shortlinks/main.go +++ b/cmd/shortlinks/main.go @@ -102,14 +102,11 @@ func RunServer(ctx context.Context, log logger.Logger, tracer trace.Tracer, conf linkGroup.POST("/new", NewShortlinkCreateHandler(log, shortlinkService, host)) linkGroup.GET("/:linkId", NewShortlinkResolveHandler(log, shortlinkService)) - grpcObj := &ShortlinksGrpc{ - log: log, - host: host, - shortlinkService: shortlinkService, - } - grpcUnderlying := grpc.NewServer() - shortlinks.RegisterShortlinksServer(grpcUnderlying, grpcObj) + shortlinks.RegisterShortlinksServer( + grpcUnderlying, + NewShortlinksGrpc(log, shortlinkService, host), + ) httpServer := httpserver.New( httpserver.NewServerOpts{ diff --git a/internal/http_server/wrapper.go b/internal/http_server/wrapper.go new file mode 100644 index 0000000..0f866ee --- /dev/null +++ b/internal/http_server/wrapper.go @@ -0,0 +1,79 @@ +package httpserver + +import ( + "backend/pkg/logger" + "context" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +type Handler[Input, Output any] func(ctx context.Context, input Input) (Output, error) + +type ResponseOk struct { + Status string `json:"status"` + Result interface{} `json:"result"` +} + +type ResponseError struct { + Status string `json:"status"` + Error struct { + Id string `json:"id"` + Message string `json:"message"` + } `json:"error"` +} + +func WrapGin[In, Out interface{}](log logger.Logger, handler Handler[In, Out]) gin.HandlerFunc { + return func(c *gin.Context) { + log := log.WithContext(c) + + var input In + if err := c.ShouldBindJSON(&input); err != nil { + response := ResponseError{ + Status: "error", + Error: struct { + Id string `json:"id"` + Message string `json:"message"` + }{ + Id: "WrongBody", + Message: err.Error(), + }, + } + + body, _ := json.Marshal(response) + c.Data(400, "application/json", body) + return + } + + var response interface{} + + output, err := handler(c, input) + if err != nil { + log.Error().Err(err).Msg("error in request handler") + response = ResponseError{ + Status: "error", + Error: struct { + Id string `json:"id"` + Message string `json:"message"` + }{ + Id: "-", + Message: err.Error(), + }, + } + } else { + response = ResponseOk{ + Status: "success", + Result: output, + } + } + + body, err := json.Marshal(response) + if err != nil { + log.Error().Err(err).Msg("marshal response error") + c.Data(500, "plain/text", []byte(err.Error())) + return + } + + c.Data(200, "application/json", body) + } +}