diff --git a/delivery/http_server/admin/admin/refresh_access.go b/delivery/http_server/admin/admin/refresh_access.go new file mode 100644 index 0000000..4c96011 --- /dev/null +++ b/delivery/http_server/admin/admin/refresh_access.go @@ -0,0 +1,37 @@ +package adminhandler + +import ( + adminserviceparam "git.gocasts.ir/ebhomengo/niki/param/admin/admin" + "net/http" + + httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg" + "github.com/labstack/echo/v4" +) + +// RefreshAccess godoc +// @Summary Get a new access token by providing a refresh token +// @Tags Admins +// @Accept json +// @Produce json +// @Param Request body adminserviceparam.RefreshAccessRequest true "Refresh access request body" +// @Success 200 {object} adminserviceparam.RefreshAccessResponse +// @Failure 400 {string} "Bad Request" +// @Failure 422 {string} "invalid or expired jwt" +// @Failure 500 {string} "something went wrong" +// @Router /admins/refresh-access [post]. +func (h Handler) RefreshAccess(c echo.Context) error { + var req adminserviceparam.RefreshAccessRequest + + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest) + } + + resp, err := h.adminSvc.RefreshAccess(c.Request().Context(), req) + if err != nil { + msg, code := httpmsg.Error(err) + + return echo.NewHTTPError(code, msg) + } + + return c.JSON(http.StatusOK, resp) +} diff --git a/delivery/http_server/admin/admin/route.go b/delivery/http_server/admin/admin/route.go index 4463f7d..2d3f820 100644 --- a/delivery/http_server/admin/admin/route.go +++ b/delivery/http_server/admin/admin/route.go @@ -13,6 +13,7 @@ func (h Handler) SetRoutes(e *echo.Echo) { //r.POST("/", h.Add).Name = "admin-addkindboxreq" r.POST("/register", h.Register, middleware.Auth(h.authSvc), middleware.AdminAuthorization(h.adminAuthorizeSvc, entity.AdminAdminRegisterPermission)) r.POST("/login-by-phone", h.LoginByPhoneNumber) + r.POST("/refresh-access", h.RefreshAccess) //nolint:gocritic //r.PATCH("/:id", h.Update).Name = "admin-updatekindboxreq" } diff --git a/delivery/http_server/benefactor/benefactor/refresh_access.go b/delivery/http_server/benefactor/benefactor/refresh_access.go new file mode 100644 index 0000000..ac56507 --- /dev/null +++ b/delivery/http_server/benefactor/benefactor/refresh_access.go @@ -0,0 +1,36 @@ +package benefactorhandler + +import ( + benefactorparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" + httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg" + "net/http" + + "github.com/labstack/echo/v4" +) + +// RefreshAccess godoc +// @Summary Get a new access token by providing your refresh token +// @Tags Benefactors +// @Accept json +// @Produce json +// @Param Request body benefactorparam.RefreshAccessRequest true "Refresh access token request body" +// @Success 200 {object} benefactorparam.RefreshAccessResponse +// @Failure 400 {string} "Bad Request" +// @Failure 500 {string} "something went wrong" +// @Router /benefactors/refresh-access [post]. +func (h Handler) RefreshAccess(c echo.Context) error { + var req benefactorparam.RefreshAccessRequest + + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest) + } + + resp, err := h.benefactorSvc.RefreshAccess(c.Request().Context(), req) + if err != nil { + msg, code := httpmsg.Error(err) + + return echo.NewHTTPError(code, msg) + } + + return c.JSON(http.StatusOK, resp) +} diff --git a/delivery/http_server/benefactor/benefactor/route.go b/delivery/http_server/benefactor/benefactor/route.go index d8c8baa..9eb624a 100644 --- a/delivery/http_server/benefactor/benefactor/route.go +++ b/delivery/http_server/benefactor/benefactor/route.go @@ -9,4 +9,5 @@ func (h Handler) SetRoutes(e *echo.Echo) { r.POST("/send-otp", h.SendOtp) r.POST("/login-register", h.loginOrRegister) + r.POST("/refresh-access", h.RefreshAccess) } diff --git a/delivery/http_server/middleware/auth.go b/delivery/http_server/middleware/auth.go index 5d7eb94..1f9e74d 100644 --- a/delivery/http_server/middleware/auth.go +++ b/delivery/http_server/middleware/auth.go @@ -14,7 +14,7 @@ func Auth(service authservice.Service) echo.MiddlewareFunc { // TODO - as sign method string to config SigningMethod: "HS256", ParseTokenFunc: func(c echo.Context, auth string) (interface{}, error) { - claims, err := service.ParseToken(auth) + claims, err := service.ParseBearerToken(auth) if err != nil { return nil, err } diff --git a/docs/docs.go b/docs/docs.go index ba10048..6e1efc1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1235,6 +1235,57 @@ const docTemplate = `{ } } }, + "/admins/refresh-access": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admins" + ], + "summary": "Get a new access token by providing a refresh token", + "parameters": [ + { + "description": "Refresh access request body", + "name": "Request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/adminserviceparam.RefreshAccessRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/adminserviceparam.RefreshAccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "422": { + "description": "invalid or expired jwt", + "schema": { + "type": "string" + } + }, + "500": { + "description": "something went wrong", + "schema": { + "type": "string" + } + } + } + } + }, "/admins/register": { "post": { "security": [ @@ -2487,6 +2538,51 @@ const docTemplate = `{ } } }, + "/benefactors/refresh-access": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Benefactors" + ], + "summary": "Get a new access token by providing your refresh token", + "parameters": [ + { + "description": "Refresh access token request body", + "name": "Request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/benefactoreparam.RefreshAccessRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/benefactoreparam.RefreshAccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "something went wrong", + "schema": { + "type": "string" + } + } + } + } + }, "/benefactors/send-otp": { "post": { "description": "This endpoint sends an OTP to the benefactor's phone number for verification purposes.", @@ -3218,6 +3314,22 @@ const docTemplate = `{ } } }, + "adminserviceparam.RefreshAccessRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "adminserviceparam.RefreshAccessResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + } + } + }, "adminserviceparam.RegisterRequest": { "type": "object", "properties": { @@ -3449,6 +3561,22 @@ const docTemplate = `{ } } }, + "benefactoreparam.RefreshAccessRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "benefactoreparam.RefreshAccessResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + } + } + }, "benefactoreparam.SendOtpRequest": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index e8c878b..9d25024 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1224,6 +1224,57 @@ } } }, + "/admins/refresh-access": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admins" + ], + "summary": "Get a new access token by providing a refresh token", + "parameters": [ + { + "description": "Refresh access request body", + "name": "Request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/adminserviceparam.RefreshAccessRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/adminserviceparam.RefreshAccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "422": { + "description": "invalid or expired jwt", + "schema": { + "type": "string" + } + }, + "500": { + "description": "something went wrong", + "schema": { + "type": "string" + } + } + } + } + }, "/admins/register": { "post": { "security": [ @@ -2476,6 +2527,51 @@ } } }, + "/benefactors/refresh-access": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Benefactors" + ], + "summary": "Get a new access token by providing your refresh token", + "parameters": [ + { + "description": "Refresh access token request body", + "name": "Request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/benefactoreparam.RefreshAccessRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/benefactoreparam.RefreshAccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "something went wrong", + "schema": { + "type": "string" + } + } + } + } + }, "/benefactors/send-otp": { "post": { "description": "This endpoint sends an OTP to the benefactor's phone number for verification purposes.", @@ -3207,6 +3303,22 @@ } } }, + "adminserviceparam.RefreshAccessRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "adminserviceparam.RefreshAccessResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + } + } + }, "adminserviceparam.RegisterRequest": { "type": "object", "properties": { @@ -3438,6 +3550,22 @@ } } }, + "benefactoreparam.RefreshAccessRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "benefactoreparam.RefreshAccessResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + } + } + }, "benefactoreparam.SendOtpRequest": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b735c50..2718fd9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -455,6 +455,16 @@ definitions: tokens: $ref: '#/definitions/adminserviceparam.Tokens' type: object + adminserviceparam.RefreshAccessRequest: + properties: + refresh_token: + type: string + type: object + adminserviceparam.RefreshAccessResponse: + properties: + access_token: + type: string + type: object adminserviceparam.RegisterRequest: properties: description: @@ -604,6 +614,16 @@ definitions: tokens: $ref: '#/definitions/benefactoreparam.Tokens' type: object + benefactoreparam.RefreshAccessRequest: + properties: + refresh_token: + type: string + type: object + benefactoreparam.RefreshAccessResponse: + properties: + access_token: + type: string + type: object benefactoreparam.SendOtpRequest: properties: phone_number: @@ -1721,6 +1741,39 @@ paths: summary: "Admin login by\tPhoneNumber" tags: - Admins + /admins/refresh-access: + post: + consumes: + - application/json + parameters: + - description: Refresh access request body + in: body + name: Request + required: true + schema: + $ref: '#/definitions/adminserviceparam.RefreshAccessRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/adminserviceparam.RefreshAccessResponse' + "400": + description: Bad Request + schema: + type: string + "422": + description: invalid or expired jwt + schema: + type: string + "500": + description: something went wrong + schema: + type: string + summary: Get a new access token by providing a refresh token + tags: + - Admins /admins/register: post: consumes: @@ -2547,6 +2600,35 @@ paths: summary: Login or register a benefactor tags: - Benefactors + /benefactors/refresh-access: + post: + consumes: + - application/json + parameters: + - description: Refresh access token request body + in: body + name: Request + required: true + schema: + $ref: '#/definitions/benefactoreparam.RefreshAccessRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/benefactoreparam.RefreshAccessResponse' + "400": + description: Bad Request + schema: + type: string + "500": + description: something went wrong + schema: + type: string + summary: Get a new access token by providing your refresh token + tags: + - Benefactors /benefactors/send-otp: post: consumes: diff --git a/entity/benefactor.go b/entity/benefactor.go index 543d56f..064c51c 100644 --- a/entity/benefactor.go +++ b/entity/benefactor.go @@ -12,4 +12,5 @@ type Benefactor struct { Gender Gender BirthDate time.Time Role UserRole + Status BenefactorStatus } diff --git a/entity/benefactor_status.go b/entity/benefactor_status.go new file mode 100644 index 0000000..ab9eb03 --- /dev/null +++ b/entity/benefactor_status.go @@ -0,0 +1,19 @@ +package entity + +type BenefactorStatus string + +const ( + BenefactorActiveStatus = BenefactorStatus("active") + BenefactorInactiveStatus = BenefactorStatus("inactive") +) + +var BenefactorStatusStrings = map[BenefactorStatus]string{ + BenefactorActiveStatus: "active", + BenefactorInactiveStatus: "inactive", +} + +func (b BenefactorStatus) IsValid() bool { + _, ok := BenefactorStatusStrings[b] + + return ok +} diff --git a/param/admin/admin/refresh_access.go b/param/admin/admin/refresh_access.go new file mode 100644 index 0000000..cbf30bb --- /dev/null +++ b/param/admin/admin/refresh_access.go @@ -0,0 +1,9 @@ +package adminserviceparam + +type RefreshAccessRequest struct { + RefreshToken string `json:"refresh_token"` +} + +type RefreshAccessResponse struct { + AccessToken string `json:"access_token"` +} diff --git a/param/benefactor/benefactor/refresh_access.go b/param/benefactor/benefactor/refresh_access.go new file mode 100644 index 0000000..773b481 --- /dev/null +++ b/param/benefactor/benefactor/refresh_access.go @@ -0,0 +1,9 @@ +package benefactoreparam + +type RefreshAccessRequest struct { + RefreshToken string `json:"refresh_token"` +} + +type RefreshAccessResponse struct { + AccessToken string `json:"access_token"` +} diff --git a/pkg/err_msg/message.go b/pkg/err_msg/message.go index 8ec7e87..17474a4 100644 --- a/pkg/err_msg/message.go +++ b/pkg/err_msg/message.go @@ -37,4 +37,6 @@ const ( ErrorMsgAssignReceiverAgentKindBoxStatus = "only ready to return kindboxes can be assigned to a receiver agent" ErrorMsgReturnKindBoxStatus = "only returned kindboxes can be enumerated" ErrorMsgInvalidSerialNumberRange = "invalid serial number range" + ErrorMsgInvalidOrExpiredJwt = "invalid or expired jwt" + ErrorMsgInvalidRefreshToken = "invalid refresh token" ) diff --git a/repository/mysql/admin/exist.go b/repository/mysql/admin/exist.go index 029127c..e062c38 100644 --- a/repository/mysql/admin/exist.go +++ b/repository/mysql/admin/exist.go @@ -83,14 +83,11 @@ func (d *DB) GetAdminByID(ctx context.Context, adminID uint) (entity.Admin, erro row := stmt.QueryRowContext(ctx, adminID) admin, err := scanAdmin(row) if err != nil { - sErr := sql.ErrNoRows - //TODO-errorsas: second argument to errors.As should not be *error - //nolint - if errors.As(err, &sErr) { - return entity.Admin{}, nil + if errors.Is(err, sql.ErrNoRows) { + return entity.Admin{}, richerror.New(op).WithKind(richerror.KindNotFound). + WithMessage(errmsg.ErrorMsgNotFound) } - // TODO - log unexpected error for better observability return entity.Admin{}, richerror.New(op).WithErr(err). WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) } diff --git a/repository/mysql/admin/get.go b/repository/mysql/admin/get.go index 3f7c0c0..a8e8526 100644 --- a/repository/mysql/admin/get.go +++ b/repository/mysql/admin/get.go @@ -26,15 +26,11 @@ func (d *DB) GetAdminByPhoneNumber(ctx context.Context, phoneNumber string) (ent row := stmt.QueryRowContext(ctx, phoneNumber) admin, err := scanAdmin(row) if err != nil { - sErr := sql.ErrNoRows - //TODO-errorsas: second argument to errors.As should not be *error - //nolint - if errors.As(err, &sErr) { - return entity.Admin{}, richerror.New(op).WithErr(sErr). + if errors.Is(err, sql.ErrNoRows) { + return entity.Admin{}, richerror.New(op). WithMessage(errmsg.ErrorMsgNotFound).WithKind(richerror.KindNotFound) } - // TODO - log unexpected error for better observability return entity.Admin{}, richerror.New(op).WithErr(err). WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) } diff --git a/repository/mysql/benefactor/exist.go b/repository/mysql/benefactor/exist.go index 9df7c8b..aa1a65a 100644 --- a/repository/mysql/benefactor/exist.go +++ b/repository/mysql/benefactor/exist.go @@ -2,142 +2,39 @@ package mysqlbenefactor import ( "context" - "database/sql" "errors" - "time" - "git.gocasts.ir/ebhomengo/niki/entity" - errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" - "git.gocasts.ir/ebhomengo/niki/repository/mysql" ) func (d *DB) IsExistBenefactorByPhoneNumber(ctx context.Context, phoneNumber string) (bool, entity.Benefactor, error) { const op = "mysqlbenefactor.IsExistBenefactorByPhoneNumber" - query := `select * from benefactors where phone_number = ?` - //nolint - stmt, err := d.conn.PrepareStatement(ctx, mysql.StatementKeyBenefactorIsExistByPhoneNumber, query) + bnf, err := d.GetByPhoneNumber(ctx, phoneNumber) if err != nil { - return false, entity.Benefactor{}, richerror.New(op).WithErr(err). - WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) - } - - row := stmt.QueryRowContext(ctx, phoneNumber) - Benefactor, err := scanBenefactor(row) - if err != nil { - sErr := sql.ErrNoRows - //TODO-errorsas: second argument to errors.As should not be *error - //nolint - if errors.As(err, &sErr) { + var richErr richerror.RichError + if errors.As(err, &richErr) && richErr.Kind() == richerror.KindNotFound { return false, entity.Benefactor{}, nil } - // TODO - log unexpected error for better observability - return false, entity.Benefactor{}, richerror.New(op).WithErr(err). - WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) + return false, entity.Benefactor{}, richerror.New(op).WithErr(err) } - return true, Benefactor, nil + return true, bnf, nil } func (d *DB) IsExistBenefactorByID(ctx context.Context, id uint) (bool, error) { const op = "mysqlbenefactor.IsExistBenefactorByID" - query := `select * from benefactors where id = ?` - //nolint - stmt, err := d.conn.PrepareStatement(ctx, mysql.StatementKeyBenefactorIsExistByID, query) + _, err := d.GetByID(ctx, id) if err != nil { - return false, richerror.New(op).WithErr(err). - WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) - } - - row := stmt.QueryRowContext(ctx, id) - _, err = scanBenefactor(row) - if err != nil { - sErr := sql.ErrNoRows - //TODO-errorsas: second argument to errors.As should not be *error - //nolint - if errors.As(err, &sErr) { + var richErr richerror.RichError + if errors.As(err, &richErr) && richErr.Kind() == richerror.KindNotFound { return false, nil } - // TODO - log unexpected error for better observability - return false, richerror.New(op).WithErr(err). - WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) + return false, richerror.New(op).WithErr(err) } return true, nil } - -func scanBenefactor(scanner mysql.Scanner) (entity.Benefactor, error) { - var createdAt, updatedAt time.Time - var benefactor entity.Benefactor - // TODO - use db model and mapper between entity and db model OR use this approach - - var benefactorNullableFields nullableFields - - err := scanner.Scan(&benefactor.ID, &benefactorNullableFields.firstName, - &benefactorNullableFields.lastName, &benefactor.PhoneNumber, &benefactorNullableFields.description, - &benefactorNullableFields.email, &benefactorNullableFields.genderStr, - &benefactorNullableFields.birthdate, &createdAt, &updatedAt) - - mapNotNullToBenefactor(benefactorNullableFields, &benefactor) - - return benefactor, err -} - -type nullableFields struct { - firstName sql.NullString - lastName sql.NullString - description sql.NullString - email sql.NullString - genderStr sql.NullString - birthdate sql.NullTime -} - -// TODO - find the other solution. -func mapNotNullToBenefactor(data nullableFields, benefactor *entity.Benefactor) { - if data.firstName.Valid { - benefactor.FirstName = data.firstName.String - } - if data.lastName.Valid { - benefactor.LastName = data.lastName.String - } - - if data.description.Valid { - benefactor.Description = data.description.String - } - if data.email.Valid { - benefactor.Email = data.email.String - } - - if data.genderStr.Valid { - benefactor.Gender = entity.Gender(data.genderStr.String) - } - if data.birthdate.Valid { - benefactor.BirthDate = data.birthdate.Time - } -} - -func (d *DB) GetByID(ctx context.Context, benefactorID uint) (entity.Benefactor, error) { - const op = "mysqlbenefactor.IsExistBenefactorByID" - - row := d.conn.Conn().QueryRowContext(ctx, `select * from benefactors where id = ?`, benefactorID) - - bnf, err := scanBenefactor(row) - if err != nil { - sErr := sql.ErrNoRows - //TODO-errorsas: second argument to errors.As should not be *error - //nolint - if errors.As(err, &sErr) { - return bnf, nil - } - - // TODO - log unexpected error for better observability - return bnf, richerror.New(op).WithErr(err). - WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) - } - - return bnf, nil -} diff --git a/repository/mysql/benefactor/get.go b/repository/mysql/benefactor/get.go new file mode 100644 index 0000000..789f4b3 --- /dev/null +++ b/repository/mysql/benefactor/get.go @@ -0,0 +1,63 @@ +package mysqlbenefactor + +import ( + "context" + "database/sql" + "errors" + "git.gocasts.ir/ebhomengo/niki/entity" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" +) + +func (d *DB) GetByID(ctx context.Context, id uint) (entity.Benefactor, error) { + const op = "mysqlbenefactor.GetByID" + + query := `select * from benefactors where id = ?` + //nolint + stmt, err := d.conn.PrepareStatement(ctx, mysql.StatementKeyBenefactorGetByID, query) + if err != nil { + return entity.Benefactor{}, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) + } + + row := stmt.QueryRowContext(ctx, id) + bnf, err := scanBenefactor(row) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return entity.Benefactor{}, richerror.New(op).WithKind(richerror.KindNotFound). + WithMessage(errmsg.ErrorMsgNotFound) + } + + return entity.Benefactor{}, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) + } + + return bnf, nil +} + +func (d *DB) GetByPhoneNumber(ctx context.Context, phoneNumber string) (entity.Benefactor, error) { + const op = "mysqlbenefactor.GetByPhoneNumber" + + query := `select * from benefactors where phone_number = ?` + //nolint + stmt, err := d.conn.PrepareStatement(ctx, mysql.StatementKeyBenefactorGetByPhoneNumber, query) + if err != nil { + return entity.Benefactor{}, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) + } + + row := stmt.QueryRowContext(ctx, phoneNumber) + bnf, err := scanBenefactor(row) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return entity.Benefactor{}, richerror.New(op).WithKind(richerror.KindNotFound). + WithMessage(errmsg.ErrorMsgNotFound) + } + + return entity.Benefactor{}, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) + } + + return bnf, nil +} diff --git a/repository/mysql/benefactor/scan.go b/repository/mysql/benefactor/scan.go new file mode 100644 index 0000000..a6ad125 --- /dev/null +++ b/repository/mysql/benefactor/scan.go @@ -0,0 +1,58 @@ +package mysqlbenefactor + +import ( + "database/sql" + "git.gocasts.ir/ebhomengo/niki/entity" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" + "time" +) + +func scanBenefactor(scanner mysql.Scanner) (entity.Benefactor, error) { + var createdAt, updatedAt time.Time + var benefactor entity.Benefactor + // TODO - use db model and mapper between entity and db model OR use this approach + + var benefactorNullableFields nullableFields + + err := scanner.Scan(&benefactor.ID, &benefactorNullableFields.firstName, + &benefactorNullableFields.lastName, &benefactor.PhoneNumber, &benefactorNullableFields.description, + &benefactorNullableFields.email, &benefactorNullableFields.genderStr, + &benefactorNullableFields.birthdate, &createdAt, &updatedAt, &benefactor.Status) + + mapNotNullToBenefactor(benefactorNullableFields, &benefactor) + + return benefactor, err +} + +type nullableFields struct { + firstName sql.NullString + lastName sql.NullString + description sql.NullString + email sql.NullString + genderStr sql.NullString + birthdate sql.NullTime +} + +// TODO - find the other solution. +func mapNotNullToBenefactor(data nullableFields, benefactor *entity.Benefactor) { + if data.firstName.Valid { + benefactor.FirstName = data.firstName.String + } + if data.lastName.Valid { + benefactor.LastName = data.lastName.String + } + + if data.description.Valid { + benefactor.Description = data.description.String + } + if data.email.Valid { + benefactor.Email = data.email.String + } + + if data.genderStr.Valid { + benefactor.Gender = entity.Gender(data.genderStr.String) + } + if data.birthdate.Valid { + benefactor.BirthDate = data.birthdate.Time + } +} diff --git a/repository/mysql/migration/1726083157_add_status_for_benefactors.sql b/repository/mysql/migration/1726083157_add_status_for_benefactors.sql new file mode 100644 index 0000000..070138f --- /dev/null +++ b/repository/mysql/migration/1726083157_add_status_for_benefactors.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `benefactors` ADD COLUMN `status` ENUM('active','inactive') NOT NULL DEFAULT 'active'; + +-- +migrate Down +ALTER TABLE `benefactors` DROP COLUMN `status`; \ No newline at end of file diff --git a/repository/mysql/prepared_statement.go b/repository/mysql/prepared_statement.go index 9500522..6bfb01e 100644 --- a/repository/mysql/prepared_statement.go +++ b/repository/mysql/prepared_statement.go @@ -22,8 +22,8 @@ const ( StatementKeyAdminGetByID StatementKeyAdminGetByPhoneNumber StatementKeyAdminAgentGetAll - StatementKeyBenefactorIsExistByID - StatementKeyBenefactorIsExistByPhoneNumber + StatementKeyBenefactorGetByID + StatementKeyBenefactorGetByPhoneNumber StatementKeyBenefactorCreate StatementKeyKindBoxAdd StatementKeyKindBoxAssignReceiverAgent diff --git a/service/admin/admin/refresh_access.go b/service/admin/admin/refresh_access.go new file mode 100644 index 0000000..49034a4 --- /dev/null +++ b/service/admin/admin/refresh_access.go @@ -0,0 +1,35 @@ +package adminservice + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/entity" + adminserviceparam "git.gocasts.ir/ebhomengo/niki/param/admin/admin" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (s Service) RefreshAccess(ctx context.Context, req adminserviceparam.RefreshAccessRequest) (adminserviceparam.RefreshAccessResponse, error) { + const op = "adminservice.RefreshAccess" + claims, err := s.auth.ParseRefreshToken(req.RefreshToken) + if err != nil { + return adminserviceparam.RefreshAccessResponse{}, richerror.New(op).WithKind(richerror.KindInvalid).WithMessage(errmsg.ErrorMsgInvalidOrExpiredJwt) + } + + admin, err := s.repo.GetAdminByID(ctx, claims.UserID) + if err != nil { + return adminserviceparam.RefreshAccessResponse{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) + } + if admin.Status == entity.AdminInactiveStatus { + return adminserviceparam.RefreshAccessResponse{}, richerror.New(op).WithKind(richerror.KindForbidden).WithMessage(errmsg.ErrorMsgAdminNotAllowed) + } + + accessToken, err := s.auth.CreateAccessToken(entity.Authenticable{ + ID: claims.UserID, + Role: claims.Role, + }) + if err != nil { + return adminserviceparam.RefreshAccessResponse{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) + } + + return adminserviceparam.RefreshAccessResponse{AccessToken: accessToken}, nil +} diff --git a/service/admin/admin/service.go b/service/admin/admin/service.go index eacc527..4e3d18f 100644 --- a/service/admin/admin/service.go +++ b/service/admin/admin/service.go @@ -3,6 +3,7 @@ package adminservice import ( "context" "fmt" + "git.gocasts.ir/ebhomengo/niki/service/auth" "git.gocasts.ir/ebhomengo/niki/config" "git.gocasts.ir/ebhomengo/niki/entity" @@ -10,9 +11,10 @@ import ( "golang.org/x/crypto/bcrypt" ) -type AuthGenerator interface { +type AuthService interface { CreateAccessToken(admin entity.Authenticable) (string, error) CreateRefreshToken(admin entity.Authenticable) (string, error) + ParseRefreshToken(refreshToken string) (*auth.Claims, error) } type Repository interface { @@ -23,11 +25,11 @@ type Repository interface { type Service struct { repo Repository - auth AuthGenerator + auth AuthService vld validator.Validator } -func New(repo Repository, auth AuthGenerator, vld validator.Validator) Service { +func New(repo Repository, auth AuthService, vld validator.Validator) Service { return Service{ repo: repo, auth: auth, diff --git a/service/auth/create.go b/service/auth/create.go new file mode 100644 index 0000000..957ed40 --- /dev/null +++ b/service/auth/create.go @@ -0,0 +1,39 @@ +package auth + +import ( + "git.gocasts.ir/ebhomengo/niki/entity" + "github.com/golang-jwt/jwt/v4" + "time" +) + +func (s Service) CreateAccessToken(user entity.Authenticable) (string, error) { + return s.createToken(user.ID, user.Role, s.Config.AccessSubject, s.Config.AccessExpirationTime) +} + +func (s Service) CreateRefreshToken(user entity.Authenticable) (string, error) { + return s.createToken(user.ID, user.Role, s.Config.RefreshSubject, s.Config.RefreshExpirationTime) +} + +func (s Service) createToken(userID uint, role, subject string, expireDuration time.Duration) (string, error) { + // create a signer for rsa 256 + // TODO - replace with rsa 256 RS256 - https://github.com/golang-jwt/jwt/blob/main/http_example_test.go + + // set our claims + claims := Claims{ + RegisteredClaims: jwt.RegisteredClaims{ + Subject: subject, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireDuration)), + }, + UserID: userID, + Role: role, + } + + // TODO - add sign method to config + accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := accessToken.SignedString([]byte(s.Config.SignKey)) + if err != nil { + return "", err + } + + return tokenString, nil +} diff --git a/service/auth/parse.go b/service/auth/parse.go new file mode 100644 index 0000000..c472280 --- /dev/null +++ b/service/auth/parse.go @@ -0,0 +1,42 @@ +package auth + +import ( + "fmt" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + "github.com/golang-jwt/jwt/v4" + "strings" +) + +func (s Service) ParseBearerToken(bearerToken string) (*Claims, error) { + tokenStr := strings.Replace(bearerToken, "Bearer ", "", 1) + + return s.parseToken(tokenStr) +} + +func (s Service) ParseRefreshToken(refreshToken string) (*Claims, error) { + claims, err := s.parseToken(refreshToken) + if err != nil { + return nil, err + } + if claims.Subject != s.Config.RefreshSubject { + return nil, fmt.Errorf(errmsg.ErrorMsgInvalidRefreshToken) + } + + return claims, nil +} + +func (s Service) parseToken(token string) (*Claims, error) { + // https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-ParseWithClaims-CustomClaimsType + t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(s.Config.SignKey), nil + }) + if err != nil { + return nil, err + } + + if claims, ok := t.Claims.(*Claims); ok && t.Valid { + return claims, nil + } + + return nil, err +} diff --git a/service/auth/service.go b/service/auth/service.go index 636352c..9170f07 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -1,11 +1,7 @@ package auth import ( - "strings" "time" - - "git.gocasts.ir/ebhomengo/niki/entity" - "github.com/golang-jwt/jwt/v4" ) type Config struct { @@ -25,54 +21,3 @@ func New(cfg Config) Service { Config: cfg, } } - -func (s Service) CreateAccessToken(user entity.Authenticable) (string, error) { - return s.createToken(user.ID, user.Role, s.Config.AccessSubject, s.Config.AccessExpirationTime) -} - -func (s Service) CreateRefreshToken(user entity.Authenticable) (string, error) { - return s.createToken(user.ID, user.Role, s.Config.RefreshSubject, s.Config.RefreshExpirationTime) -} - -func (s Service) ParseToken(bearerToken string) (*Claims, error) { - // https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-ParseWithClaims-CustomClaimsType - - tokenStr := strings.Replace(bearerToken, "Bearer ", "", 1) - - token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(s.Config.SignKey), nil - }) - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(*Claims); ok && token.Valid { - return claims, nil - } - - return nil, err -} - -func (s Service) createToken(userID uint, role, subject string, expireDuration time.Duration) (string, error) { - // create a signer for rsa 256 - // TODO - replace with rsa 256 RS256 - https://github.com/golang-jwt/jwt/blob/main/http_example_test.go - - // set our claims - claims := Claims{ - RegisteredClaims: jwt.RegisteredClaims{ - Subject: subject, - ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireDuration)), - }, - UserID: userID, - Role: role, - } - - // TODO - add sign method to config - accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := accessToken.SignedString([]byte(s.Config.SignKey)) - if err != nil { - return "", err - } - - return tokenString, nil -} diff --git a/service/benefactor/benefactor/refresh_access.go b/service/benefactor/benefactor/refresh_access.go new file mode 100644 index 0000000..ded6ef3 --- /dev/null +++ b/service/benefactor/benefactor/refresh_access.go @@ -0,0 +1,35 @@ +package benefactorservice + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/entity" + benefactorparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (s Service) RefreshAccess(ctx context.Context, req benefactorparam.RefreshAccessRequest) (benefactorparam.RefreshAccessResponse, error) { + const op = "adminservice.RefreshAccess" + claims, err := s.auth.ParseRefreshToken(req.RefreshToken) + if err != nil { + return benefactorparam.RefreshAccessResponse{}, richerror.New(op).WithKind(richerror.KindInvalid).WithMessage(errmsg.ErrorMsgInvalidOrExpiredJwt) + } + + benefactor, err := s.repo.GetByID(ctx, claims.UserID) + if err != nil { + return benefactorparam.RefreshAccessResponse{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) + } + if benefactor.Status == entity.BenefactorInactiveStatus { + return benefactorparam.RefreshAccessResponse{}, richerror.New(op).WithKind(richerror.KindForbidden).WithMessage(errmsg.ErrorMsgUserNotAllowed) + } + + accessToken, err := s.auth.CreateAccessToken(entity.Authenticable{ + ID: claims.UserID, + Role: claims.Role, + }) + if err != nil { + return benefactorparam.RefreshAccessResponse{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) + } + + return benefactorparam.RefreshAccessResponse{AccessToken: accessToken}, nil +} diff --git a/service/benefactor/benefactor/send_otp.go b/service/benefactor/benefactor/send_otp.go index 202013d..9afbff3 100644 --- a/service/benefactor/benefactor/send_otp.go +++ b/service/benefactor/benefactor/send_otp.go @@ -12,7 +12,7 @@ import ( func (s Service) SendOtp(ctx context.Context, req benefactoreparam.SendOtpRequest) (benefactoreparam.SendOtpResponse, error) { const op = "benefactorservice.SendOtp" - if fieldErrors, vErr := s.vld.ValidateSendOtpRequest(req); vErr != nil { + if fieldErrors, vErr := s.vld.ValidateSendOtpRequest(ctx, req); vErr != nil { return benefactoreparam.SendOtpResponse{FieldErrors: fieldErrors}, richerror.New(op).WithErr(vErr) } diff --git a/service/benefactor/benefactor/service.go b/service/benefactor/benefactor/service.go index 3865c9a..c92dd88 100644 --- a/service/benefactor/benefactor/service.go +++ b/service/benefactor/benefactor/service.go @@ -2,6 +2,7 @@ package benefactorservice import ( "context" + "git.gocasts.ir/ebhomengo/niki/service/auth" "time" smscontract "git.gocasts.ir/ebhomengo/niki/contract/sms" @@ -22,9 +23,10 @@ type Repository interface { GetByID(ctx context.Context, benefactorID uint) (entity.Benefactor, error) } -type AuthGenerator interface { - CreateAccessToken(benefactor entity.Authenticable) (string, error) - CreateRefreshToken(benefactor entity.Authenticable) (string, error) +type AuthService interface { + CreateAccessToken(admin entity.Authenticable) (string, error) + CreateRefreshToken(admin entity.Authenticable) (string, error) + ParseRefreshToken(refreshToken string) (*auth.Claims, error) } type RedisOtp interface { @@ -38,13 +40,13 @@ type Service struct { config Config redisOtp RedisOtp smsAdapter smscontract.SmsAdapter - auth AuthGenerator + auth AuthService repo Repository vld benefactorvalidator.Validator } func New(cfg Config, redisOtp RedisOtp, smsAdapter smscontract.SmsAdapter, - auth AuthGenerator, repo Repository, vld benefactorvalidator.Validator, + auth AuthService, repo Repository, vld benefactorvalidator.Validator, ) Service { return Service{ config: cfg, diff --git a/service/service.go b/service/service.go index f00ae19..336ee68 100644 --- a/service/service.go +++ b/service/service.go @@ -89,7 +89,7 @@ func New(cfg config.Config, db *mysql.DB, rds *redis.Adapter, smsAdapter smscont BenefactorAuthSvc = auth.New(cfg.BenefactorAuth) BenefactorReferTimeSvc = benefactorrefertimeservice.New(referTimeRepo) - BenefactorVld = benefactorvalidator.New() + BenefactorVld = benefactorvalidator.New(benefactorRepo) BenefactorSvc = benefactorservice.New(cfg.BenefactorSvc, redisOtp, smsAdapter, BenefactorAuthSvc, benefactorRepo, BenefactorVld) BenefactorAddressVld = benefactoraddressvalidator.New(BenefactorSvc, addressRepo) BenefactorAddressSvc = benefactoraddressservice.New(addressRepo, BenefactorAddressVld) diff --git a/validator/admin/admin/login.go b/validator/admin/admin/login.go index 6254c23..3b44858 100644 --- a/validator/admin/admin/login.go +++ b/validator/admin/admin/login.go @@ -12,7 +12,7 @@ import ( ) func (v Validator) ValidateLoginWithPhoneNumberRequest(ctx context.Context, req adminserviceparam.LoginWithPhoneNumberRequest) (map[string]string, error) { - const op = "adminvalidator.ValidateRegisterRequest" + const op = "adminvalidator.ValidateLoginWithPhoneNumberRequest" if err := validation.ValidateStruct(&req, // TODO - add regex @@ -22,7 +22,9 @@ func (v Validator) ValidateLoginWithPhoneNumberRequest(ctx context.Context, req validation.Field(&req.PhoneNumber, validation.Required, validation.Match(regexp.MustCompile(phoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid), - validation.By(v.doesAdminExistByPhoneNumber(ctx)))); err != nil { + validation.By(v.doesAdminExistByPhoneNumber(ctx)), + validation.By(v.isAdminAllowed(ctx)), + )); err != nil { fieldErrors := make(map[string]string) vErr := validation.Errors{} diff --git a/validator/admin/admin/login_test.go b/validator/admin/admin/login_test.go index 6c08607..5d3077c 100644 --- a/validator/admin/admin/login_test.go +++ b/validator/admin/admin/login_test.go @@ -3,6 +3,7 @@ package adminvalidator import ( "context" "errors" + "git.gocasts.ir/ebhomengo/niki/entity" "testing" adminserviceparam "git.gocasts.ir/ebhomengo/niki/param/admin/admin" @@ -23,7 +24,10 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) { Password: validPassword, } - mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(true, nil).Once() + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, req.PhoneNumber).Return(true, nil).Once() + mockRepo.EXPECT().GetAdminByPhoneNumber(ctx, req.PhoneNumber).Return(entity.Admin{ + Status: entity.AdminActiveStatus, + }, nil).Once() fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) assert.NoError(t, err) @@ -48,7 +52,10 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) { Password: "", } - mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(true, nil).Once() + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, req.PhoneNumber).Return(true, nil).Once() + mockRepo.EXPECT().GetAdminByPhoneNumber(ctx, req.PhoneNumber).Return(entity.Admin{ + Status: entity.AdminActiveStatus, + }, nil).Once() fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) assert.Error(t, err) @@ -74,7 +81,10 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) { Password: "short", } - mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(true, nil).Once() + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, req.PhoneNumber).Return(true, nil).Once() + mockRepo.EXPECT().GetAdminByPhoneNumber(ctx, req.PhoneNumber).Return(entity.Admin{ + Status: entity.AdminActiveStatus, + }, nil).Once() fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) assert.Error(t, err) @@ -88,7 +98,7 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) { Password: validPassword, } - mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(false, nil).Once() + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, req.PhoneNumber).Return(false, nil).Once() fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) assert.Error(t, err) @@ -102,11 +112,28 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) { Password: validPassword, } - mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(false, errors.New("repo error")).Once() + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, req.PhoneNumber).Return(false, errors.New("repo error")).Once() fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) assert.Error(t, err) assert.NotNil(t, fieldErrors) assert.Equal(t, errmsg.ErrorMsgSomethingWentWrong, fieldErrors["phone_number"]) }) + + t.Run("Inactive admin is not allowed", func(t *testing.T) { + req := adminserviceparam.LoginWithPhoneNumberRequest{ + PhoneNumber: validPhoneNumber, + Password: validPassword, + } + + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, req.PhoneNumber).Return(true, nil).Once() + mockRepo.EXPECT().GetAdminByPhoneNumber(ctx, req.PhoneNumber).Return(entity.Admin{ + Status: entity.AdminInactiveStatus, + }, nil).Once() + + fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) + assert.Error(t, err) + assert.NotNil(t, fieldErrors) + assert.Equal(t, errmsg.ErrorMsgAdminNotAllowed, fieldErrors["phone_number"]) + }) } diff --git a/validator/admin/admin/mock_Repository_test.go b/validator/admin/admin/mock_Repository_test.go index d400c45..cc7080f 100644 --- a/validator/admin/admin/mock_Repository_test.go +++ b/validator/admin/admin/mock_Repository_test.go @@ -1,10 +1,11 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package adminvalidator import ( context "context" + entity "git.gocasts.ir/ebhomengo/niki/entity" mock "github.com/stretchr/testify/mock" ) @@ -135,6 +136,63 @@ func (_c *MockRepository_AdminExistByPhoneNumber_Call) RunAndReturn(run func(con return _c } +// GetAdminByPhoneNumber provides a mock function with given fields: ctx, phoneNumber +func (_m *MockRepository) GetAdminByPhoneNumber(ctx context.Context, phoneNumber string) (entity.Admin, error) { + ret := _m.Called(ctx, phoneNumber) + + if len(ret) == 0 { + panic("no return value specified for GetAdminByPhoneNumber") + } + + var r0 entity.Admin + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (entity.Admin, error)); ok { + return rf(ctx, phoneNumber) + } + if rf, ok := ret.Get(0).(func(context.Context, string) entity.Admin); ok { + r0 = rf(ctx, phoneNumber) + } else { + r0 = ret.Get(0).(entity.Admin) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, phoneNumber) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepository_GetAdminByPhoneNumber_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAdminByPhoneNumber' +type MockRepository_GetAdminByPhoneNumber_Call struct { + *mock.Call +} + +// GetAdminByPhoneNumber is a helper method to define mock.On call +// - ctx context.Context +// - phoneNumber string +func (_e *MockRepository_Expecter) GetAdminByPhoneNumber(ctx interface{}, phoneNumber interface{}) *MockRepository_GetAdminByPhoneNumber_Call { + return &MockRepository_GetAdminByPhoneNumber_Call{Call: _e.mock.On("GetAdminByPhoneNumber", ctx, phoneNumber)} +} + +func (_c *MockRepository_GetAdminByPhoneNumber_Call) Run(run func(ctx context.Context, phoneNumber string)) *MockRepository_GetAdminByPhoneNumber_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockRepository_GetAdminByPhoneNumber_Call) Return(_a0 entity.Admin, _a1 error) *MockRepository_GetAdminByPhoneNumber_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRepository_GetAdminByPhoneNumber_Call) RunAndReturn(run func(context.Context, string) (entity.Admin, error)) *MockRepository_GetAdminByPhoneNumber_Call { + _c.Call.Return(run) + return _c +} + // NewMockRepository creates a new instance of MockRepository. 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 NewMockRepository(t interface { diff --git a/validator/admin/admin/validator.go b/validator/admin/admin/validator.go index e583bc0..47312f8 100644 --- a/validator/admin/admin/validator.go +++ b/validator/admin/admin/validator.go @@ -3,7 +3,6 @@ package adminvalidator import ( "context" "fmt" - "git.gocasts.ir/ebhomengo/niki/entity" errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" validation "github.com/go-ozzo/ozzo-validation/v4" @@ -23,6 +22,7 @@ const ( type Repository interface { AdminExistByPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) AdminExistByEmail(ctx context.Context, email string) (bool, error) + GetAdminByPhoneNumber(ctx context.Context, phoneNumber string) (entity.Admin, error) } type Validator struct { @@ -33,6 +33,24 @@ func New(repo Repository) Validator { return Validator{repo: repo} } +func (v Validator) isAdminAllowed(ctx context.Context) func(interface{}) error { + return func(value interface{}) error { + phoneNumber, ok := value.(string) + if !ok { + return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong) + } + admin, err := v.repo.GetAdminByPhoneNumber(ctx, phoneNumber) + if err != nil { + return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong) + } + if admin.Status == entity.AdminInactiveStatus { + return fmt.Errorf(errmsg.ErrorMsgAdminNotAllowed) + } + + return nil + } +} + func (v Validator) doesAdminExistByPhoneNumber(ctx context.Context) validation.RuleFunc { return func(value interface{}) error { phoneNumber, ok := value.(string) diff --git a/validator/admin/kind_box/mock_AddressSvc_test.go b/validator/admin/kind_box/mock_AddressSvc_test.go index f83fd35..09a0f9f 100644 --- a/validator/admin/kind_box/mock_AddressSvc_test.go +++ b/validator/admin/kind_box/mock_AddressSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.45.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package adminkindboxvalidator diff --git a/validator/admin/kind_box/mock_AgentSvc_test.go b/validator/admin/kind_box/mock_AgentSvc_test.go index 81b3eb2..70b14bf 100644 --- a/validator/admin/kind_box/mock_AgentSvc_test.go +++ b/validator/admin/kind_box/mock_AgentSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.45.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package adminkindboxvalidator diff --git a/validator/admin/kind_box/mock_BenefactorSvc_test.go b/validator/admin/kind_box/mock_BenefactorSvc_test.go index f9419a0..fa83e4d 100644 --- a/validator/admin/kind_box/mock_BenefactorSvc_test.go +++ b/validator/admin/kind_box/mock_BenefactorSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.45.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package adminkindboxvalidator diff --git a/validator/admin/kind_box/mock_ReferTimeSvc_test.go b/validator/admin/kind_box/mock_ReferTimeSvc_test.go index 4ef06a9..4f340f0 100644 --- a/validator/admin/kind_box/mock_ReferTimeSvc_test.go +++ b/validator/admin/kind_box/mock_ReferTimeSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.45.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package adminkindboxvalidator diff --git a/validator/admin/kind_box/mock_Repository_test.go b/validator/admin/kind_box/mock_Repository_test.go index 6715b17..4c632d2 100644 --- a/validator/admin/kind_box/mock_Repository_test.go +++ b/validator/admin/kind_box/mock_Repository_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.45.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package adminkindboxvalidator diff --git a/validator/benefactor/address/mock_BenefactorSvc_test.go b/validator/benefactor/address/mock_BenefactorSvc_test.go index 4da745b..1d22449 100644 --- a/validator/benefactor/address/mock_BenefactorSvc_test.go +++ b/validator/benefactor/address/mock_BenefactorSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactoraddressvalidator @@ -6,6 +6,7 @@ import ( context "context" benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" + mock "github.com/stretchr/testify/mock" ) diff --git a/validator/benefactor/address/mock_Repository_test.go b/validator/benefactor/address/mock_Repository_test.go index f80f859..74029f3 100644 --- a/validator/benefactor/address/mock_Repository_test.go +++ b/validator/benefactor/address/mock_Repository_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactoraddressvalidator diff --git a/validator/benefactor/benefactor/login_register_test.go b/validator/benefactor/benefactor/login_register_test.go index 317e693..18eaf19 100644 --- a/validator/benefactor/benefactor/login_register_test.go +++ b/validator/benefactor/benefactor/login_register_test.go @@ -9,7 +9,8 @@ import ( ) func TestValidator_ValidateLoginRegisterRequest(t *testing.T) { - validator := New() + mockRepository := NewMockRepository(t) + validator := New(mockRepository) validPhoneNumber := "09123456789" t.Run("Valid request", func(t *testing.T) { diff --git a/validator/benefactor/benefactor/mock_Repository_test.go b/validator/benefactor/benefactor/mock_Repository_test.go new file mode 100644 index 0000000..318a24f --- /dev/null +++ b/validator/benefactor/benefactor/mock_Repository_test.go @@ -0,0 +1,94 @@ +// Code generated by mockery v2.45.1. DO NOT EDIT. + +package benefactorvalidator + +import ( + context "context" + + entity "git.gocasts.ir/ebhomengo/niki/entity" + mock "github.com/stretchr/testify/mock" +) + +// MockRepository is an autogenerated mock type for the Repository type +type MockRepository struct { + mock.Mock +} + +type MockRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRepository) EXPECT() *MockRepository_Expecter { + return &MockRepository_Expecter{mock: &_m.Mock} +} + +// GetByPhoneNumber provides a mock function with given fields: ctx, phoneNumber +func (_m *MockRepository) GetByPhoneNumber(ctx context.Context, phoneNumber string) (entity.Benefactor, error) { + ret := _m.Called(ctx, phoneNumber) + + if len(ret) == 0 { + panic("no return value specified for GetByPhoneNumber") + } + + var r0 entity.Benefactor + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (entity.Benefactor, error)); ok { + return rf(ctx, phoneNumber) + } + if rf, ok := ret.Get(0).(func(context.Context, string) entity.Benefactor); ok { + r0 = rf(ctx, phoneNumber) + } else { + r0 = ret.Get(0).(entity.Benefactor) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, phoneNumber) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepository_GetByPhoneNumber_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByPhoneNumber' +type MockRepository_GetByPhoneNumber_Call struct { + *mock.Call +} + +// GetByPhoneNumber is a helper method to define mock.On call +// - ctx context.Context +// - phoneNumber string +func (_e *MockRepository_Expecter) GetByPhoneNumber(ctx interface{}, phoneNumber interface{}) *MockRepository_GetByPhoneNumber_Call { + return &MockRepository_GetByPhoneNumber_Call{Call: _e.mock.On("GetByPhoneNumber", ctx, phoneNumber)} +} + +func (_c *MockRepository_GetByPhoneNumber_Call) Run(run func(ctx context.Context, phoneNumber string)) *MockRepository_GetByPhoneNumber_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockRepository_GetByPhoneNumber_Call) Return(_a0 entity.Benefactor, _a1 error) *MockRepository_GetByPhoneNumber_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRepository_GetByPhoneNumber_Call) RunAndReturn(run func(context.Context, string) (entity.Benefactor, error)) *MockRepository_GetByPhoneNumber_Call { + _c.Call.Return(run) + return _c +} + +// NewMockRepository creates a new instance of MockRepository. 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 NewMockRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRepository { + mock := &MockRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/validator/benefactor/benefactor/send_otp.go b/validator/benefactor/benefactor/send_otp.go index 487ab64..a5a4e5a 100644 --- a/validator/benefactor/benefactor/send_otp.go +++ b/validator/benefactor/benefactor/send_otp.go @@ -1,6 +1,7 @@ package benefactorvalidator import ( + "context" "errors" "regexp" @@ -10,13 +11,15 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" ) -func (v Validator) ValidateSendOtpRequest(req benefactoreparam.SendOtpRequest) (map[string]string, error) { +func (v Validator) ValidateSendOtpRequest(ctx context.Context, req benefactoreparam.SendOtpRequest) (map[string]string, error) { const op = "benefactorvalidator.ValidateSendOtpRequest" if err := validation.ValidateStruct(&req, validation.Field(&req.PhoneNumber, validation.Required, - validation.Match(regexp.MustCompile(phoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid))); err != nil { + validation.Match(regexp.MustCompile(phoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid), + validation.By(v.isBenefactorAllowed(ctx)), + )); err != nil { fieldErrors := make(map[string]string) vErr := validation.Errors{} diff --git a/validator/benefactor/benefactor/send_otp_test.go b/validator/benefactor/benefactor/send_otp_test.go index e39d877..02ce180 100644 --- a/validator/benefactor/benefactor/send_otp_test.go +++ b/validator/benefactor/benefactor/send_otp_test.go @@ -1,23 +1,32 @@ package benefactorvalidator import ( - "testing" - + "context" + "git.gocasts.ir/ebhomengo/niki/entity" benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" "github.com/stretchr/testify/assert" + "testing" ) func TestValidator_ValidateSendOtpRequest(t *testing.T) { - validator := New() + mockRepository := NewMockRepository(t) + validator := New(mockRepository) + validPhoneNumber := "09123456789" + ctx := context.Background() t.Run("Valid request", func(t *testing.T) { req := benefactoreparam.SendOtpRequest{ PhoneNumber: validPhoneNumber, } - fieldErrors, err := validator.ValidateSendOtpRequest(req) + mockRepository.EXPECT().GetByPhoneNumber(ctx, req.PhoneNumber).Return(entity.Benefactor{ + Status: entity.BenefactorActiveStatus, + }, nil).Once() + + fieldErrors, err := validator.ValidateSendOtpRequest(ctx, req) assert.NoError(t, err) assert.Nil(t, fieldErrors) }) @@ -27,7 +36,7 @@ func TestValidator_ValidateSendOtpRequest(t *testing.T) { PhoneNumber: "", } - fieldErrors, err := validator.ValidateSendOtpRequest(req) + fieldErrors, err := validator.ValidateSendOtpRequest(ctx, req) assert.Error(t, err) assert.NotNil(t, fieldErrors) assert.Contains(t, fieldErrors, "phone_number") @@ -38,9 +47,37 @@ func TestValidator_ValidateSendOtpRequest(t *testing.T) { PhoneNumber: "12345", } - fieldErrors, err := validator.ValidateSendOtpRequest(req) + fieldErrors, err := validator.ValidateSendOtpRequest(ctx, req) assert.Error(t, err) assert.NotNil(t, fieldErrors) assert.Equal(t, errmsg.ErrorMsgPhoneNumberIsNotValid, fieldErrors["phone_number"]) }) + + t.Run("Inactive user is not allowed", func(t *testing.T) { + req := benefactoreparam.SendOtpRequest{ + PhoneNumber: validPhoneNumber, + } + + mockRepository.EXPECT().GetByPhoneNumber(ctx, req.PhoneNumber).Return(entity.Benefactor{ + Status: entity.BenefactorInactiveStatus, + }, nil).Once() + + fieldErrors, err := validator.ValidateSendOtpRequest(ctx, req) + assert.Error(t, err) + assert.NotNil(t, fieldErrors) + assert.Equal(t, errmsg.ErrorMsgUserNotAllowed, fieldErrors["phone_number"]) + }) + + t.Run("new users are allowed", func(t *testing.T) { + req := benefactoreparam.SendOtpRequest{ + PhoneNumber: validPhoneNumber, + } + + mockRepository.EXPECT().GetByPhoneNumber(ctx, req.PhoneNumber).Return(entity.Benefactor{}, richerror.New("test"). + WithKind(richerror.KindNotFound)).Once() + + fieldErrors, err := validator.ValidateSendOtpRequest(ctx, req) + assert.NoError(t, err) + assert.Nil(t, fieldErrors) + }) } diff --git a/validator/benefactor/benefactor/validator.go b/validator/benefactor/benefactor/validator.go index 5fe5015..07b8837 100644 --- a/validator/benefactor/benefactor/validator.go +++ b/validator/benefactor/benefactor/validator.go @@ -1,11 +1,51 @@ package benefactorvalidator +import ( + "context" + "errors" + "fmt" + "git.gocasts.ir/ebhomengo/niki/entity" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + const ( phoneNumberRegex = "^09\\d{9}$" ) -type Validator struct{} - -func New() Validator { - return Validator{} +//go:generate mockery --name Repository +type Repository interface { + GetByPhoneNumber(ctx context.Context, phoneNumber string) (entity.Benefactor, error) +} + +type Validator struct { + repo Repository +} + +func New(repo Repository) Validator { + return Validator{repo: repo} +} + +func (v Validator) isBenefactorAllowed(ctx context.Context) func(interface{}) error { + return func(value interface{}) error { + phoneNumber, ok := value.(string) + if !ok { + return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong) + } + bnf, err := v.repo.GetByPhoneNumber(ctx, phoneNumber) + if err != nil { + // new users are always allowed + var richErr richerror.RichError + if errors.As(err, &richErr) && richErr.Kind() == richerror.KindNotFound { + return nil + } + + return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong) + } + if bnf.Status == entity.BenefactorInactiveStatus { + return fmt.Errorf(errmsg.ErrorMsgUserNotAllowed) + } + + return nil + } } diff --git a/validator/benefactor/kind_box/mock_AddressSvc_test.go b/validator/benefactor/kind_box/mock_AddressSvc_test.go index 7158211..988a1ba 100644 --- a/validator/benefactor/kind_box/mock_AddressSvc_test.go +++ b/validator/benefactor/kind_box/mock_AddressSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactorkindboxvalidator @@ -6,6 +6,7 @@ import ( context "context" addressparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/address" + mock "github.com/stretchr/testify/mock" ) diff --git a/validator/benefactor/kind_box/mock_BenefactorSvc_test.go b/validator/benefactor/kind_box/mock_BenefactorSvc_test.go index 9f6a4d9..fa9ab26 100644 --- a/validator/benefactor/kind_box/mock_BenefactorSvc_test.go +++ b/validator/benefactor/kind_box/mock_BenefactorSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactorkindboxvalidator @@ -6,6 +6,7 @@ import ( context "context" benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" + mock "github.com/stretchr/testify/mock" ) diff --git a/validator/benefactor/kind_box/mock_ReferTimeSvc_test.go b/validator/benefactor/kind_box/mock_ReferTimeSvc_test.go index cb1d958..d5d40e7 100644 --- a/validator/benefactor/kind_box/mock_ReferTimeSvc_test.go +++ b/validator/benefactor/kind_box/mock_ReferTimeSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactorkindboxvalidator diff --git a/validator/benefactor/kind_box/mock_Repository_test.go b/validator/benefactor/kind_box/mock_Repository_test.go index 68970d9..ad500d1 100644 --- a/validator/benefactor/kind_box/mock_Repository_test.go +++ b/validator/benefactor/kind_box/mock_Repository_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactorkindboxvalidator diff --git a/validator/benefactor/kind_box_req/mock_AddressSvc_test.go b/validator/benefactor/kind_box_req/mock_AddressSvc_test.go index 824bbbe..279fddf 100644 --- a/validator/benefactor/kind_box_req/mock_AddressSvc_test.go +++ b/validator/benefactor/kind_box_req/mock_AddressSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactorkindboxreqvalidator @@ -6,6 +6,7 @@ import ( context "context" addressparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/address" + mock "github.com/stretchr/testify/mock" ) diff --git a/validator/benefactor/kind_box_req/mock_BenefactorSvc_test.go b/validator/benefactor/kind_box_req/mock_BenefactorSvc_test.go index a08ca6e..6c98a95 100644 --- a/validator/benefactor/kind_box_req/mock_BenefactorSvc_test.go +++ b/validator/benefactor/kind_box_req/mock_BenefactorSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactorkindboxreqvalidator @@ -6,6 +6,7 @@ import ( context "context" benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" + mock "github.com/stretchr/testify/mock" ) diff --git a/validator/benefactor/kind_box_req/mock_ReferTimeSvc_test.go b/validator/benefactor/kind_box_req/mock_ReferTimeSvc_test.go index 1d923e9..11e309c 100644 --- a/validator/benefactor/kind_box_req/mock_ReferTimeSvc_test.go +++ b/validator/benefactor/kind_box_req/mock_ReferTimeSvc_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactorkindboxreqvalidator diff --git a/validator/benefactor/kind_box_req/mock_Repository_test.go b/validator/benefactor/kind_box_req/mock_Repository_test.go index d5dff1a..6005bd4 100644 --- a/validator/benefactor/kind_box_req/mock_Repository_test.go +++ b/validator/benefactor/kind_box_req/mock_Repository_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.41.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package benefactorkindboxreqvalidator