diff --git a/.golangci.yaml b/.golangci.yaml index b76c410..628e316 100755 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -34,6 +34,7 @@ linters: - dogsled - dupl - errcheck + - errorlint - exportloopref - forbidigo - funlen diff --git a/cmd/auth_service/internal/services/user_gorm_service.go b/cmd/auth_service/internal/services/user_gorm_service.go index 821b1e4..8a2c394 100755 --- a/cmd/auth_service/internal/services/user_gorm_service.go +++ b/cmd/auth_service/internal/services/user_gorm_service.go @@ -13,17 +13,17 @@ type ( Load(ctx context.Context, id string) (*models.UserDBModel, error) } userService struct { - repository repository.UserRepository + repo repository.UserRepository } ) -func NewORMUserService(repository repository.UserRepository) UserService { - return &userService{repository: repository} +func NewORMUserService(repo repository.UserRepository) UserService { + return &userService{repo: repo} } func (s *userService) Load(ctx context.Context, id string) (*models.UserDBModel, error) { uid, _ := strconv.ParseInt(id, 10, 64) - res, err := s.repository.GetOrCreate(ctx, uid) + res, err := s.repo.GetOrCreate(ctx, uid) if err != nil { return nil, err } diff --git a/cmd/gateway/internal/handler/auth.go b/cmd/gateway/internal/handler/auth.go index 9763e4c..08563e6 100644 --- a/cmd/gateway/internal/handler/auth.go +++ b/cmd/gateway/internal/handler/auth.go @@ -29,31 +29,33 @@ type ( GetVerificationCode() http.HandlerFunc } authHandler struct { - cqrs *cqrs.Application - logger *logger.Logger + application *cqrs.Application + logger *logger.Logger } ) -func (a authHandler) RegisterRoutes(r *mux.Router, cfg interface{}) { +const verificationCodeParam = "code" + +func (h authHandler) RegisterRoutes(r *mux.Router, cfg interface{}) { params := cfg.(auth.Auth) authorizer, _ := auth.NewAuthorizer(¶ms) sr := r.PathPrefix("/auth/").Subrouter() - sr.Methods(http.MethodPost).Path("/signup").HandlerFunc(authorizer.Middleware(a.SignUpUser())) - sr.Methods(http.MethodPost).Path("/signin/{auth_code}").HandlerFunc(authorizer.Middleware(a.SignInUserByCode())) - sr.Methods(http.MethodPost).Path("/signin").HandlerFunc(authorizer.Middleware(a.SignInUser())) + sr.Methods(http.MethodPost).Path("/signup").HandlerFunc(authorizer.Middleware(h.SignUpUser())) + sr.Methods(http.MethodPost).Path("/signin/{auth_code}").HandlerFunc(authorizer.Middleware(h.SignInUserByCode())) + sr.Methods(http.MethodPost).Path("/signin").HandlerFunc(authorizer.Middleware(h.SignInUser())) - sr.Methods(http.MethodGet).Path("/verify/{code}").HandlerFunc(authorizer.Middleware(a.Verify())) - sr.Methods(http.MethodPost).Path("/code").HandlerFunc(authorizer.Middleware(a.GetVerificationCode())) - sr.Methods(http.MethodGet).Path("/code/{code}").HandlerFunc(authorizer.Middleware(a.GetUserByCode())) + sr.Methods(http.MethodGet).Path("/verify/{code}").HandlerFunc(authorizer.Middleware(h.Verify())) + sr.Methods(http.MethodPost).Path("/code").HandlerFunc(authorizer.Middleware(h.GetVerificationCode())) + sr.Methods(http.MethodGet).Path("/code/{code}").HandlerFunc(authorizer.Middleware(h.GetUserByCode())) } -func NewAuthHandler(cqrs *cqrs.Application, l *logger.Logger) AuthHandler { - return authHandler{cqrs, l} +func NewAuthHandler(application *cqrs.Application, l *logger.Logger) AuthHandler { + return authHandler{application, l} } -func (a authHandler) SignInUser() http.HandlerFunc { +func (h authHandler) SignInUser() http.HandlerFunc { reqUser := models.SignInUserRequest{} res := &models.UserResponse{} @@ -62,17 +64,17 @@ func (a authHandler) SignInUser() http.HandlerFunc { defer span.End() if err := validate.UserInput(r, &reqUser); err != nil { - a.logger.Error().Err(err).Msg("SignInUser: validate") + h.logger.Error().Err(err).Msg("SignInUser: validate") responses.RespondBadRequest(w, err.Error()) return } var errQuery error - res, errQuery = a.cqrs.SigninCommand(ctx, reqUser) + res, errQuery = h.application.SigninCommand(ctx, reqUser) if errQuery != nil { - a.logger.Error().Err(errQuery).Msg("SignInUser: grpc signIn") + h.logger.Error().Err(errQuery).Msg("SignInUser: grpc signIn") if e, ok := status.FromError(errQuery); ok { responses.FromGRPCError(e, w) @@ -85,7 +87,7 @@ func (a authHandler) SignInUser() http.HandlerFunc { } } -func (a authHandler) SignInUserByCode() http.HandlerFunc { +func (h authHandler) SignInUserByCode() http.HandlerFunc { var authCode string reqSignIn := models.VerificationCodeRequest{} res := &models.UserResponse{} @@ -103,17 +105,17 @@ func (a authHandler) SignInUserByCode() http.HandlerFunc { } if err := validate.UserInput(r, &reqSignIn); err != nil { tracing.RecordError(span, err) - a.logger.Error().Err(err).Msg("SignInUserByCode: decode") + h.logger.Error().Err(err).Msg("SignInUserByCode: decode") responses.RespondBadRequest(w, err.Error()) return } var errQuery error - res, errQuery = a.cqrs.SigninByCodeCommand(ctx, reqSignIn.Email, authCode) + res, errQuery = h.application.SigninByCodeCommand(ctx, reqSignIn.Email, authCode) if errQuery != nil { - a.logger.Error().Err(errQuery).Msg("SignInUser: grpc signIn") + h.logger.Error().Err(errQuery).Msg("SignInUser: grpc signIn") if e, ok := status.FromError(errQuery); ok { responses.FromGRPCError(e, w) @@ -126,27 +128,26 @@ func (a authHandler) SignInUserByCode() http.HandlerFunc { } } -func (a authHandler) SignUpUser() http.HandlerFunc { +func (h authHandler) SignUpUser() http.HandlerFunc { var reqUser models.SignUpUserRequest return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.GetTracerProvider().Tracer("Handler").Start(r.Context(), "Handler/SignUpUser") + ctx, span := otel.GetTracerProvider().Tracer("Handler").Start(r.Context(), "AuthHandler/SignUpUser") defer span.End() if err := validate.UserInput(r, &reqUser); err != nil { tracing.RecordError(span, err) - a.logger.Error().Err(err).Msg("SignUpUser: decode") + h.logger.Error().Err(err).Msg("SignUpUser: validate") responses.RespondBadRequest(w, err.Error()) return } - err := a.cqrs.SignupUserCommand(ctx, reqUser) + err := h.application.SignupUserCommand(ctx, reqUser) if err != nil { - span.RecordError(err, trace.WithStackTrace(true)) - span.SetStatus(codes.Error, err.Error()) - a.logger.Error().Err(err).Msg("SignUpUser:create") + tracing.RecordError(span, err) + h.logger.Error().Err(err).Msg("SignUpUser:create") if e, ok := status.FromError(err); ok { responses.FromGRPCError(e, w) @@ -159,7 +160,7 @@ func (a authHandler) SignUpUser() http.HandlerFunc { } } -func (a authHandler) GetVerificationCode() http.HandlerFunc { +func (h authHandler) GetVerificationCode() http.HandlerFunc { reqSignIn := models.VerificationCodeRequest{} resp := models.UserResponse{} @@ -169,17 +170,17 @@ func (a authHandler) GetVerificationCode() http.HandlerFunc { if err := validate.UserInput(r, &reqSignIn); err != nil { tracing.RecordError(span, err) - a.logger.Error().Err(err).Msg("GetVerificationCode: validate") + h.logger.Error().Err(err).Msg("GetVerificationCode: validate") responses.RespondBadRequest(w, err.Error()) return } - _, err := a.cqrs.GetUser(ctx, models.UserRequest{Email: reqSignIn.Email}) + _, err := h.application.GetUser(ctx, models.UserRequest{Email: reqSignIn.Email}) if err != nil { span.RecordError(err, trace.WithStackTrace(true)) span.SetStatus(codes.Error, err.Error()) - a.logger.Error().Err(err).Msg("GetVerificationCode: fetchUser") + h.logger.Error().Err(err).Msg("GetVerificationCode: fetchUser") if e, ok := status.FromError(err); ok { responses.FromGRPCError(e, w) @@ -189,11 +190,11 @@ func (a authHandler) GetVerificationCode() http.HandlerFunc { return } - resp, err = a.cqrs.GetVerificationCode(ctx, reqSignIn.Email) + resp, err = h.application.GetVerificationCode(ctx, reqSignIn.Email) if err != nil { span.RecordError(err, trace.WithStackTrace(true)) span.SetStatus(codes.Error, err.Error()) - a.logger.Error().Err(err).Msg("GetVerificationCode: GetVerificationCode") + h.logger.Error().Err(err).Msg("GetVerificationCode: GetVerificationCode") if e, ok := status.FromError(err); ok { responses.FromGRPCError(e, w) @@ -206,27 +207,27 @@ func (a authHandler) GetVerificationCode() http.HandlerFunc { } } -func (a authHandler) GetUserByCode() http.HandlerFunc { +func (h authHandler) GetUserByCode() http.HandlerFunc { var vCode string return func(w http.ResponseWriter, r *http.Request) { ctx, span := otel.GetTracerProvider().Tracer("user-handler").Start(r.Context(), "GetUserByCode") defer span.End() - vCode = mux.Vars(r)["code"] + vCode = mux.Vars(r)[verificationCodeParam] if vCode == "" { - vCode = r.URL.Query().Get("code") + vCode = r.URL.Query().Get(verificationCodeParam) if vCode == "" { responses.RespondBadRequest(w, "code param is missing") return } } - user, err := a.cqrs.GetUserByCode(ctx, vCode) + user, err := h.application.GetUserByCode(ctx, vCode) if err != nil { span.RecordError(err, trace.WithStackTrace(true)) span.SetStatus(codes.Error, err.Error()) - a.logger.Error().Err(err).Msg("GetUserByID:header:getId") + h.logger.Error().Err(err).Msg("GetUserByID:header:getId") responses.RespondBadRequest(w, err.Error()) return } @@ -234,7 +235,7 @@ func (a authHandler) GetUserByCode() http.HandlerFunc { if err != nil { span.RecordError(err, trace.WithStackTrace(true)) span.SetStatus(codes.Error, err.Error()) - a.logger.Error().Err(err).Msg("GetUserByID:grpc:getUser") + h.logger.Error().Err(err).Msg("GetUserByID:grpc:getUser") if e, ok := status.FromError(err); ok { responses.FromGRPCError(e, w) @@ -247,29 +248,29 @@ func (a authHandler) GetUserByCode() http.HandlerFunc { } } -func (a authHandler) Verify() http.HandlerFunc { +func (h authHandler) Verify() http.HandlerFunc { var vCode string return func(w http.ResponseWriter, r *http.Request) { ctx, span := otel.GetTracerProvider().Tracer("auth-handler").Start(r.Context(), "Handler SignUpUser") defer span.End() - vCode = mux.Vars(r)["code"] + vCode = mux.Vars(r)[verificationCodeParam] if vCode == "" { - vCode = r.URL.Query().Get("code") + vCode = r.URL.Query().Get(verificationCodeParam) if vCode == "" { responses.RespondBadRequest(w, "code param is missing") return } } - err := a.cqrs.Commands.VerifyUserByCode.Handle(ctx, command.VerifyCode{VerificationCode: vCode}) + err := h.application.Commands.VerifyUserByCode.Handle(ctx, command.VerifyCode{VerificationCode: vCode}) if err != nil { span.RecordError(err, trace.WithStackTrace(true)) span.SetStatus(codes.Error, err.Error()) - a.logger.Error().Err(err).Msg("Verify") + h.logger.Error().Err(err).Msg("Verify") if e, ok := status.FromError(err); ok { responses.FromGRPCError(e, w) diff --git a/cmd/gateway/internal/handler/crypton.go b/cmd/gateway/internal/handler/crypton.go index 4134396..0712e30 100644 --- a/cmd/gateway/internal/handler/crypton.go +++ b/cmd/gateway/internal/handler/crypton.go @@ -4,12 +4,10 @@ import ( "net/http" "github.com/RafalSalwa/auth-api/pkg/encdec" + "github.com/RafalSalwa/auth-api/pkg/logger" "github.com/RafalSalwa/auth-api/pkg/responses" - "go.opentelemetry.io/otel" - "github.com/gorilla/mux" - - "github.com/RafalSalwa/auth-api/pkg/logger" + "go.opentelemetry.io/otel" ) type ( @@ -24,12 +22,16 @@ type ( } ) -func (c cryptonHandler) RegisterRoutes(r *mux.Router, cfg interface{}) { - r.Methods(http.MethodGet).Path("/encrypt/{message}").HandlerFunc(c.Encrypt()) - r.Methods(http.MethodGet).Path("/decrypt/{message}").HandlerFunc(c.Decrypt()) +func NewCryptonHandler(l *logger.Logger) CryptonHandler { + return cryptonHandler{l} +} + +func (h cryptonHandler) RegisterRoutes(r *mux.Router, cfg interface{}) { + r.Methods(http.MethodGet).Path("/encrypt/{message}").HandlerFunc(h.Encrypt()) + r.Methods(http.MethodGet).Path("/decrypt/{message}").HandlerFunc(h.Decrypt()) } -func (c cryptonHandler) Encrypt() http.HandlerFunc { +func (h cryptonHandler) Encrypt() http.HandlerFunc { var message string return func(w http.ResponseWriter, r *http.Request) { _, span := otel.GetTracerProvider().Tracer("Encrypt").Start(r.Context(), "Encrypt Handler") @@ -41,7 +43,7 @@ func (c cryptonHandler) Encrypt() http.HandlerFunc { } } -func (c cryptonHandler) Decrypt() http.HandlerFunc { +func (h cryptonHandler) Decrypt() http.HandlerFunc { var message string return func(w http.ResponseWriter, r *http.Request) { _, span := otel.GetTracerProvider().Tracer("Encrypt").Start(r.Context(), "Encrypt Handler") @@ -52,7 +54,3 @@ func (c cryptonHandler) Decrypt() http.HandlerFunc { responses.RespondString(w, encrypted) } } - -func NewCryptonHandler(l *logger.Logger) CryptonHandler { - return cryptonHandler{l} -} diff --git a/cmd/gateway/internal/handler/user.go b/cmd/gateway/internal/handler/user.go index 665ca8b..8801b7a 100755 --- a/cmd/gateway/internal/handler/user.go +++ b/cmd/gateway/internal/handler/user.go @@ -27,25 +27,25 @@ type ( PasswordChange() HandlerFunc } userHandler struct { - cqrs *cqrs.Application - logger *logger.Logger + application *cqrs.Application + logger *logger.Logger } ) -func NewUserHandler(cqrs *cqrs.Application, l *logger.Logger) UserHandler { - return userHandler{cqrs, l} +func NewUserHandler(application *cqrs.Application, l *logger.Logger) UserHandler { + return userHandler{application, l} } -func (uh userHandler) RegisterRoutes(r *mux.Router, cfg interface{}) { +func (h userHandler) RegisterRoutes(r *mux.Router, cfg interface{}) { params := cfg.(auth.JWTConfig) s := r.PathPrefix("/user").Subrouter() s.Use(middlewares.ValidateJWTAccessToken(¶ms)) - s.Methods(http.MethodGet).Path("").HandlerFunc(uh.GetUserByID()) - s.Methods(http.MethodPost).Path("/change_password").HandlerFunc(uh.PasswordChange()) + s.Methods(http.MethodGet).Path("").HandlerFunc(h.GetUserByID()) + s.Methods(http.MethodPost).Path("/change_password").HandlerFunc(h.PasswordChange()) } -func (uh userHandler) GetUserByID() HandlerFunc { +func (h userHandler) GetUserByID() HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx, span := otel.GetTracerProvider().Tracer("user-handler").Start(r.Context(), "GetUserByID") defer span.End() @@ -55,16 +55,16 @@ func (uh userHandler) GetUserByID() HandlerFunc { if err != nil { span.RecordError(err, trace.WithStackTrace(true)) span.SetStatus(codes.Error, err.Error()) - uh.logger.Error().Err(err).Msg("GetUserByID:header:getId") + h.logger.Error().Err(err).Msg("GetUserByID:header:getId") responses.RespondBadRequest(w, err.Error()) return } - user, err := uh.cqrs.GetUser(ctx, models.UserRequest{Id: userID}) + user, err := h.application.GetUser(ctx, models.UserRequest{Id: userID}) if err != nil { span.RecordError(err, trace.WithStackTrace(true)) span.SetStatus(codes.Error, err.Error()) - uh.logger.Error().Err(err).Msg("GetUserByID:grpc:getUser") + h.logger.Error().Err(err).Msg("GetUserByID:grpc:getUser") if e, ok := status.FromError(err); ok { responses.FromGRPCError(e, w) @@ -77,7 +77,7 @@ func (uh userHandler) GetUserByID() HandlerFunc { } } -func (uh userHandler) PasswordChange() HandlerFunc { +func (h userHandler) PasswordChange() HandlerFunc { req := &models.ChangePasswordRequest{} return func(w http.ResponseWriter, r *http.Request) { @@ -87,38 +87,38 @@ func (uh userHandler) PasswordChange() HandlerFunc { userID, err := strconv.ParseInt(r.Header.Get("x-user-id"), 10, 64) if err != nil { tracing.RecordError(span, err) - uh.logger.Error().Err(err).Msg("GetUserByID:header:getId") + h.logger.Error().Err(err).Msg("GetUserByID:header:getId") responses.RespondBadRequest(w, err.Error()) return } if err = validate.UserInput(r, &req); err != nil { tracing.RecordError(span, err) - uh.logger.Error().Err(err).Msg("PasswordChange: decode") + h.logger.Error().Err(err).Msg("PasswordChange: decode") responses.RespondBadRequest(w, err.Error()) return } if err = hashing.Validate(req.Password, req.PasswordConfirm); err != nil { tracing.RecordError(span, err) - uh.logger.Error().Err(err).Msg("PasswordChange:validateInputPasswords") + h.logger.Error().Err(err).Msg("PasswordChange:validateInputPasswords") responses.RespondBadRequest(w, err.Error()) return } - _, err = uh.cqrs.GetUser(ctx, models.UserRequest{Id: userID}) + _, err = h.application.GetUser(ctx, models.UserRequest{Id: userID}) if err != nil { tracing.RecordError(span, err) - uh.logger.Error().Err(err).Msg("PasswordChange:grpc:GetUserByID") + h.logger.Error().Err(err).Msg("PasswordChange:grpc:GetUserByID") responses.RespondBadRequest(w, err.Error()) return } - err = uh.cqrs.ChangePassword(ctx, req) + err = h.application.ChangePassword(ctx, req) if err != nil { span.RecordError(err, trace.WithStackTrace(true)) span.SetStatus(codes.Error, err.Error()) - uh.logger.Error().Err(err).Msg("PasswordChange:grpc:ChangePassword") + h.logger.Error().Err(err).Msg("PasswordChange:grpc:ChangePassword") if e, ok := status.FromError(err); ok { responses.FromGRPCError(e, w) diff --git a/cmd/gateway/internal/rpc_client/connection.go b/cmd/gateway/internal/rpc_client/connection.go index d954eca..5da6138 100755 --- a/cmd/gateway/internal/rpc_client/connection.go +++ b/cmd/gateway/internal/rpc_client/connection.go @@ -10,7 +10,7 @@ import ( ) func newConnection(addr string) (*grpc.ClientConn, error) { - conn, err := grpc.Dial(addr, + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithStatsHandler(otelgrpc.NewClientHandler()), grpc.WithKeepaliveParams(keepalive.ClientParameters{ diff --git a/docs/swagger.json b/docs/swagger.json index dcaa7f1..c21a673 100755 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4,7 +4,7 @@ "description": "Interview API with usage of REST,gRPC,MySQL,Redis,mongo,AMQP.

Current authorize settings is: Basic Auth with interview/interview parameters", "title": "Go Interview REST API", "contact": { - "name": "RafalSalwa", + "name": "RafalSalwa Repo", "url": "https://github.com/RafalSalwa" }, "version": "1.0" @@ -12,7 +12,7 @@ "paths": { "/auth/signup": { "post": { - "description": "Create account", + "description": "Create account with given credentials, in next step we need to obtain verification code for DOI process", "tags": [ "Auth" ], @@ -60,13 +60,16 @@ } } } + }, + "500": { + "description": "For every other action, we do not want to explicitly show any other flow details" } } } }, - "/auth/signin": { + "/auth/code": { "post": { - "description": "Log In account", + "description": "get code for user created in previous step", "tags": [ "Auth" ], @@ -75,7 +78,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignInRequest" + "$ref": "#/components/schemas/LogInRequest" } } } @@ -86,33 +89,13 @@ "content": { "application/json": { "example": { - "user": { - "username": "user1", - "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTA0NjMxNDMsImlhdCI6MTY5MDQ1OTU0MywibmJmIjoxNjkwNDU5NTQzLCJzdWIiOnsiSUQiOjEsIlVzZXJuYW1lIjoiIn19.Ly1E6KnOmRyCeRd1VhctkNUZs882rR7buG37XHPMqaGIERmYsN2y2nF5QQNyUtkTtV9Agfc10onhX8dSw1eSRg", - "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTA0ODExNDMsImlhdCI6MTY5MDQ1OTU0MywibmJmIjoxNjkwNDU5NTQzLCJzdWIiOnsiSUQiOjEsIlVzZXJuYW1lIjoiIn19.D3UfrFazNMV6al1Jgz6WGyq9g_NGZpGijTH2YrBMhUHyellXBQgBmt5GtHfDJlcuPdM2cajyhPRJ7pdYf_0Z8Q", - "created_at": "0001-01-01T00:00:00Z", - "updated_at": "0001-01-01T00:00:00Z", - "last_login": "0001-01-01T00:00:00Z", - "deleted_at": "0001-01-01T00:00:00Z" - } - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "example": { - "code": 400, - "reason": "bad request", - "message": "Key: 'LoginUserRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag" + "status": "ok" } } } }, "404": { - "description": "User not found or activated", + "description": "OK", "content": { "application/json": { "example": { @@ -121,13 +104,16 @@ } } } + }, + "500": { + "description": "For every other action, we do not want to explicitly show any other flow details" } } } }, "/auth/verify/{code}": { "get": { - "description": "Verification for account used in DOI process. This code can be obtained from email. \n
psst... check mailhog at this server and port :8025 :)", + "description": "Verification for account used in DOI process. This code can be obtained from email or auth/verify endpoint.", "tags": [ "Auth" ], @@ -175,13 +161,16 @@ } } } + }, + "500": { + "description": "For every other action, we do not want to explicitly show any other flow details" } } } }, - "/auth/code": { + "/auth/signin": { "post": { - "description": "get code for created user
(workaround in case mailhog wont be working)", + "description": "Log In account, remember to activate account (get verification code and authorize this code) in previous endpoints.", "tags": [ "Auth" ], @@ -190,7 +179,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LogInRequest" + "$ref": "#/components/schemas/SignInRequest" } } } @@ -201,13 +190,33 @@ "content": { "application/json": { "example": { - "status": "ok" + "user": { + "username": "user1", + "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTA0NjMxNDMsImlhdCI6MTY5MDQ1OTU0MywibmJmIjoxNjkwNDU5NTQzLCJzdWIiOnsiSUQiOjEsIlVzZXJuYW1lIjoiIn19.Ly1E6KnOmRyCeRd1VhctkNUZs882rR7buG37XHPMqaGIERmYsN2y2nF5QQNyUtkTtV9Agfc10onhX8dSw1eSRg", + "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTA0ODExNDMsImlhdCI6MTY5MDQ1OTU0MywibmJmIjoxNjkwNDU5NTQzLCJzdWIiOnsiSUQiOjEsIlVzZXJuYW1lIjoiIn19.D3UfrFazNMV6al1Jgz6WGyq9g_NGZpGijTH2YrBMhUHyellXBQgBmt5GtHfDJlcuPdM2cajyhPRJ7pdYf_0Z8Q", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "last_login": "0001-01-01T00:00:00Z", + "deleted_at": "0001-01-01T00:00:00Z" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "example": { + "code": 400, + "reason": "bad request", + "message": "Key: 'LoginUserRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag" } } } }, "404": { - "description": "OK", + "description": "User not found or activated", "content": { "application/json": { "example": { @@ -216,19 +225,23 @@ } } } + }, + "500": { + "description": "For every other action, we do not want to explicitly show any other flow details" } } } }, "/user": { "get": { - "description": "Log In account via jwt token from previous endpoints", + "description": "Log In account via jwt token from SignIn endpoint", "tags": [ "User" ], "security": [ { - "BearerAuth":[] + "basicAuth": [], + "BearerAuth": [] } ], "responses": { @@ -240,10 +253,24 @@ "user": { "id": 256, "username": "rafal@interview.com" - } + } + } + } + } + }, + "400": { + "description": "When token header or auth method is missing", + "content": { + "application/json": { + "example": { + "code": 400, + "reason": "bad request" } } } + }, + "500": { + "description": "For every other action, we do not want to explicitly show any other flow details" } } } @@ -293,7 +320,7 @@ }, "SignInRequest": { "properties": { - "username": { + "email": { "type": "string", "example": "rafal@interview.com" }, diff --git a/pkg/http/auth/api_key_test.go b/pkg/http/auth/api_key_test.go index c52e716..11a9b9b 100644 --- a/pkg/http/auth/api_key_test.go +++ b/pkg/http/auth/api_key_test.go @@ -1,6 +1,7 @@ package auth import ( + "context" "net/http" "net/http/httptest" "testing" @@ -15,13 +16,14 @@ func TestAPIKeyMiddleware(t *testing.T) { // Create a test server with the middleware applied apiKey := "my-api-key" + ctx := context.Background() middleware := newAPIKeyMiddleware(apiKey) handler := middleware.Middleware(mockHandler) server := httptest.NewServer(handler) defer server.Close() // Test case 1: Valid API key - req, err := http.NewRequest("GET", server.URL, nil) + req, err := http.NewRequestWithContext(ctx, "GET", server.URL, nil) if err != nil { t.Fatal(err) } @@ -36,7 +38,7 @@ func TestAPIKeyMiddleware(t *testing.T) { } // Test case 2: Missing API key - req, err = http.NewRequest("GET", server.URL, nil) + req, err = http.NewRequestWithContext(ctx, "GET", server.URL, nil) if err != nil { t.Fatal(err) } @@ -50,7 +52,7 @@ func TestAPIKeyMiddleware(t *testing.T) { } // Test case 3: Wrong API key - req, err = http.NewRequest("GET", server.URL, nil) + req, err = http.NewRequestWithContext(ctx, "GET", server.URL, nil) if err != nil { t.Fatal(err) } diff --git a/pkg/http/auth/basic_auth.go b/pkg/http/auth/basic_auth.go old mode 100755 new mode 100644 index 9738c11..22440e9 --- a/pkg/http/auth/basic_auth.go +++ b/pkg/http/auth/basic_auth.go @@ -4,6 +4,8 @@ import ( "crypto/sha256" "crypto/subtle" "net/http" + + "github.com/RafalSalwa/auth-api/pkg/responses" ) type basicAuth struct { @@ -30,6 +32,7 @@ func (a *basicAuth) Middleware(h http.HandlerFunc) http.HandlerFunc { return } } + responses.RespondNotAuthorized(w, "Missing or invalid credentials") } } diff --git a/pkg/http/auth/bearer_token.go b/pkg/http/auth/bearer_token.go old mode 100755 new mode 100644 diff --git a/pkg/http/middlewares/content_type_json.go b/pkg/http/middlewares/content_type_json.go old mode 100755 new mode 100644 index 23b1e8c..4a89622 --- a/pkg/http/middlewares/content_type_json.go +++ b/pkg/http/middlewares/content_type_json.go @@ -3,20 +3,18 @@ package middlewares import ( "net/http" + "github.com/RafalSalwa/auth-api/pkg/responses" "github.com/gorilla/mux" ) func ContentTypeJSON() mux.MiddlewareFunc { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // if r.Header.Get("Content-type") != "application/json" { - // w.WriteHeader(http.StatusUnsupportedMediaType) - // _, err := w.Write([]byte("415 - Unsupported Media Type. Only JSON files are allowed")) - // if err != nil { - // return - // } - // return - //} + if r.Header.Get("Content-type") != "application/json" && + r.Method == http.MethodPost { + responses.RespondInternalServerError(w) + return + } w.Header().Set("Content-Type", "application/json;charset=utf8") diff --git a/pkg/http/middlewares/requestlog.go b/pkg/http/middlewares/requestlog.go index ed5bc05..f8c27c3 100755 --- a/pkg/http/middlewares/requestlog.go +++ b/pkg/http/middlewares/requestlog.go @@ -17,7 +17,7 @@ func RequestLog(logger *logger.Logger) mux.MiddlewareFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() - le := &logEntry{ + entry := &logEntry{ ReceivedTime: start, RequestMethod: r.Method, RequestURL: r.URL.String(), @@ -29,7 +29,7 @@ func RequestLog(logger *logger.Logger) mux.MiddlewareFunc { } if addr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { - le.ServerIP = ipFromHostPort(addr.String()) + entry.ServerIP = ipFromHostPort(addr.String()) } body, _ := io.ReadAll(r.Body) err := r.Body.Close() @@ -44,35 +44,35 @@ func RequestLog(logger *logger.Logger) mux.MiddlewareFunc { w2 := &responseStats{w: w} r2.Body = io.NopCloser(bytes.NewBuffer(body)) - le.Latency = time.Since(start) + entry.Latency = time.Since(start) if rcc.err == nil && rcc.r != nil { _, err := io.Copy(io.Discard, rcc) if err != nil { return } } - le.RequestBodySize = rcc.n - le.Status = w2.code - if le.Status == 0 { - le.Status = http.StatusOK + entry.RequestBodySize = rcc.n + entry.Status = w2.code + if entry.Status == 0 { + entry.Status = http.StatusOK } - le.ResponseHeaderSize, le.ResponseBodySize = w2.size() - if le.RequestURL != "/metrics" { + entry.ResponseHeaderSize, entry.ResponseBodySize = w2.size() + if entry.RequestURL != "/metrics" { logger.Info(). - Time("received_time", le.ReceivedTime). - Str("method", le.RequestMethod). - Str("url", le.RequestURL). - Int64("header_size", le.RequestHeaderSize). - Int64("body_size", le.RequestBodySize). - Str("agent", le.UserAgent). - Str("referer", le.Referer). - Str("proto", le.Proto). - Str("remote_ip", le.RemoteIP). - Str("server_ip", le.ServerIP). - Int("status", le.Status). - Int64("resp_header_size", le.ResponseHeaderSize). - Int64("resp_body_size", le.ResponseBodySize). - Dur("latency", le.Latency). + Time("received_time", entry.ReceivedTime). + Str("method", entry.RequestMethod). + Str("url", entry.RequestURL). + Int64("header_size", entry.RequestHeaderSize). + Int64("body_size", entry.RequestBodySize). + Str("agent", entry.UserAgent). + Str("referer", entry.Referer). + Str("proto", entry.Proto). + Str("remote_ip", entry.RemoteIP). + Str("server_ip", entry.ServerIP). + Int("status", entry.Status). + Int64("resp_header_size", entry.ResponseHeaderSize). + Int64("resp_body_size", entry.ResponseBodySize). + Dur("latency", entry.Latency). Msg("Request") } h.ServeHTTP(w2, r2) diff --git a/pkg/models/user.go b/pkg/models/user.go index a1d885c..b0194f5 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -70,7 +70,7 @@ type ( SignUpUserRequest struct { Email string `json:"email" validate:"required,email"` Password string `json:"password" validate:"required,min=8,max=32"` - PasswordConfirm string `json:"passwordConfirm" validate:"required,min=8,max=32"` + PasswordConfirm string `json:"passwordConfirm" validate:"required,min=8,max=32,eqfield=Password"` } SignInUserRequest struct { Username string `json:"username" validate:"required_without=Email"` diff --git a/pkg/responses/response.go b/pkg/responses/response.go old mode 100755 new mode 100644 index b48879a..fa50ce3 --- a/pkg/responses/response.go +++ b/pkg/responses/response.go @@ -59,6 +59,9 @@ func RespondBadRequest(w http.ResponseWriter, msg string) { responseBody := marshalErrorResponse(errorResponse) Respond(w, http.StatusBadRequest, responseBody) } +func RespondInternalServerError(w http.ResponseWriter) { + Respond(w, http.StatusInternalServerError, []byte("")) +} func RespondString(w http.ResponseWriter, msg string) { Respond(w, http.StatusOK, []byte(msg)) diff --git a/pkg/validate/json_input.go b/pkg/validate/json_input.go new file mode 100644 index 0000000..c96347c --- /dev/null +++ b/pkg/validate/json_input.go @@ -0,0 +1,46 @@ +package validate + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/go-playground/validator/v10" +) + +type ErrEQField struct { +} + +func (e ErrEQField) Error() string { + panic("implement me") +} + +func UserInput(r *http.Request, req interface{}) error { + reqValidator := validator.New() + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return fmt.Errorf("cannot decode request data") + } + + if err := reqValidator.Struct(req); err != nil { + var validationErrors validator.ValidationErrors + if errors.As(err, &validationErrors) { + for _, fieldErr := range validationErrors { + return handleFieldError(fieldErr) + } + } + } + return nil +} + +func handleFieldError(fieldErr validator.FieldError) error { + switch fieldErr.Tag() { + case "required": + return fmt.Errorf("field '%s' is required", fieldErr.Field()) + case "eqfield": + return fmt.Errorf("different values for '%s', but should be same", fieldErr.Param()) + default: + return fmt.Errorf("field '%s' failed validation for '%s'", fieldErr.Field(), fieldErr.Tag()) + } +} diff --git a/pkg/validate/json_request.go b/pkg/validate/json_request.go new file mode 100644 index 0000000..b1a14f3 --- /dev/null +++ b/pkg/validate/json_request.go @@ -0,0 +1,79 @@ +package validate + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +type malformedRequest struct { + status int + msg string +} + +func (mr *malformedRequest) Error() string { + return mr.msg +} + +func JSONBody(w http.ResponseWriter, r *http.Request, dst any) error { + ct := r.Header.Get("Content-Type") + if ct != "" { + mediaType := strings.ToLower(strings.TrimSpace(strings.Split(ct, ";")[0])) + if mediaType != "application/json" { + msg := "Content-Type header is not application/json" + return &malformedRequest{status: http.StatusUnsupportedMediaType, msg: msg} + } + } + + r.Body = http.MaxBytesReader(w, r.Body, 1048576) + + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + err := dec.Decode(&dst) + if err != nil { + var syntaxError *json.SyntaxError + var unmarshalTypeError *json.UnmarshalTypeError + + switch { + case errors.As(err, &syntaxError): + msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset) + return &malformedRequest{status: http.StatusBadRequest, msg: msg} + + case errors.Is(err, io.ErrUnexpectedEOF): + msg := "Request body contains badly-formed JSON" + return &malformedRequest{status: http.StatusBadRequest, msg: msg} + + case errors.As(err, &unmarshalTypeError): + msg := fmt.Sprintf("Request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset) + return &malformedRequest{status: http.StatusBadRequest, msg: msg} + + case strings.HasPrefix(err.Error(), "json: unknown field "): + fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") + msg := fmt.Sprintf("Request body contains unknown field %s", fieldName) + return &malformedRequest{status: http.StatusBadRequest, msg: msg} + + case errors.Is(err, io.EOF): + msg := "Request body must not be empty" + return &malformedRequest{status: http.StatusBadRequest, msg: msg} + + case err.Error() == "http: request body too large": + msg := "Request body must not be larger than 1MB" + return &malformedRequest{status: http.StatusRequestEntityTooLarge, msg: msg} + + default: + return err + } + } + + err = dec.Decode(&struct{}{}) + if !errors.Is(err, io.EOF) { + msg := "Request body must only contain a single JSON object" + return &malformedRequest{status: http.StatusBadRequest, msg: msg} + } + + return nil +} diff --git a/pkg/validate/request_user_sign_up.go b/pkg/validate/request_user_sign_up.go deleted file mode 100755 index 0b912d0..0000000 --- a/pkg/validate/request_user_sign_up.go +++ /dev/null @@ -1,22 +0,0 @@ -package validate - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/go-playground/validator/v10" -) - -func UserInput(r *http.Request, req interface{}) error { - reqValidator := validator.New() - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return fmt.Errorf("cannot decode request data") - } - - if err := reqValidator.Struct(req); err != nil { - return fmt.Errorf("data validation failed with reason: %s", err.Error()) - } - return nil -}