forked from ebhomengo/niki
1
0
Fork 0

feat(niki): implement refresh access token for admins and benefactors (#156)

This commit is contained in:
Ruhollah 2024-09-09 11:47:30 +03:30
parent 5526088b35
commit e727bf5c0e
53 changed files with 1083 additions and 229 deletions

View File

@ -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)
}

View File

@ -13,6 +13,7 @@ func (h Handler) SetRoutes(e *echo.Echo) {
//r.POST("/", h.Add).Name = "admin-addkindboxreq" //r.POST("/", h.Add).Name = "admin-addkindboxreq"
r.POST("/register", h.Register, middleware.Auth(h.authSvc), middleware.AdminAuthorization(h.adminAuthorizeSvc, entity.AdminAdminRegisterPermission)) r.POST("/register", h.Register, middleware.Auth(h.authSvc), middleware.AdminAuthorization(h.adminAuthorizeSvc, entity.AdminAdminRegisterPermission))
r.POST("/login-by-phone", h.LoginByPhoneNumber) r.POST("/login-by-phone", h.LoginByPhoneNumber)
r.POST("/refresh-access", h.RefreshAccess)
//nolint:gocritic //nolint:gocritic
//r.PATCH("/:id", h.Update).Name = "admin-updatekindboxreq" //r.PATCH("/:id", h.Update).Name = "admin-updatekindboxreq"
} }

View File

@ -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)
}

View File

@ -9,4 +9,5 @@ func (h Handler) SetRoutes(e *echo.Echo) {
r.POST("/send-otp", h.SendOtp) r.POST("/send-otp", h.SendOtp)
r.POST("/login-register", h.loginOrRegister) r.POST("/login-register", h.loginOrRegister)
r.POST("/refresh-access", h.RefreshAccess)
} }

View File

@ -14,7 +14,7 @@ func Auth(service authservice.Service) echo.MiddlewareFunc {
// TODO - as sign method string to config // TODO - as sign method string to config
SigningMethod: "HS256", SigningMethod: "HS256",
ParseTokenFunc: func(c echo.Context, auth string) (interface{}, error) { ParseTokenFunc: func(c echo.Context, auth string) (interface{}, error) {
claims, err := service.ParseToken(auth) claims, err := service.ParseBearerToken(auth)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -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": { "/admins/register": {
"post": { "post": {
"security": [ "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": { "/benefactors/send-otp": {
"post": { "post": {
"description": "This endpoint sends an OTP to the benefactor's phone number for verification purposes.", "description": "This endpoint sends an OTP to the benefactor's phone number for verification purposes.",
@ -3214,6 +3310,22 @@ const docTemplate = `{
} }
} }
}, },
"adminserviceparam.RefreshAccessRequest": {
"type": "object",
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"adminserviceparam.RefreshAccessResponse": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
}
}
},
"adminserviceparam.RegisterRequest": { "adminserviceparam.RegisterRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3445,6 +3557,22 @@ const docTemplate = `{
} }
} }
}, },
"benefactoreparam.RefreshAccessRequest": {
"type": "object",
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"benefactoreparam.RefreshAccessResponse": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
}
}
},
"benefactoreparam.SendOtpRequest": { "benefactoreparam.SendOtpRequest": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -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": { "/admins/register": {
"post": { "post": {
"security": [ "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": { "/benefactors/send-otp": {
"post": { "post": {
"description": "This endpoint sends an OTP to the benefactor's phone number for verification purposes.", "description": "This endpoint sends an OTP to the benefactor's phone number for verification purposes.",
@ -3203,6 +3299,22 @@
} }
} }
}, },
"adminserviceparam.RefreshAccessRequest": {
"type": "object",
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"adminserviceparam.RefreshAccessResponse": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
}
}
},
"adminserviceparam.RegisterRequest": { "adminserviceparam.RegisterRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3434,6 +3546,22 @@
} }
} }
}, },
"benefactoreparam.RefreshAccessRequest": {
"type": "object",
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"benefactoreparam.RefreshAccessResponse": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
}
}
},
"benefactoreparam.SendOtpRequest": { "benefactoreparam.SendOtpRequest": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -452,6 +452,16 @@ definitions:
tokens: tokens:
$ref: '#/definitions/adminserviceparam.Tokens' $ref: '#/definitions/adminserviceparam.Tokens'
type: object type: object
adminserviceparam.RefreshAccessRequest:
properties:
refresh_token:
type: string
type: object
adminserviceparam.RefreshAccessResponse:
properties:
access_token:
type: string
type: object
adminserviceparam.RegisterRequest: adminserviceparam.RegisterRequest:
properties: properties:
description: description:
@ -601,6 +611,16 @@ definitions:
tokens: tokens:
$ref: '#/definitions/benefactoreparam.Tokens' $ref: '#/definitions/benefactoreparam.Tokens'
type: object type: object
benefactoreparam.RefreshAccessRequest:
properties:
refresh_token:
type: string
type: object
benefactoreparam.RefreshAccessResponse:
properties:
access_token:
type: string
type: object
benefactoreparam.SendOtpRequest: benefactoreparam.SendOtpRequest:
properties: properties:
phone_number: phone_number:
@ -1718,6 +1738,39 @@ paths:
summary: "Admin login by\tPhoneNumber" summary: "Admin login by\tPhoneNumber"
tags: tags:
- Admins - 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: /admins/register:
post: post:
consumes: consumes:
@ -2544,6 +2597,35 @@ paths:
summary: Login or register a benefactor summary: Login or register a benefactor
tags: tags:
- Benefactors - 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: /benefactors/send-otp:
post: post:
consumes: consumes:

View File

@ -12,4 +12,5 @@ type Benefactor struct {
Gender Gender Gender Gender
BirthDate time.Time BirthDate time.Time
Role UserRole Role UserRole
Status BenefactorStatus
} }

View File

@ -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
}

View File

@ -0,0 +1,9 @@
package adminserviceparam
type RefreshAccessRequest struct {
RefreshToken string `json:"refresh_token"`
}
type RefreshAccessResponse struct {
AccessToken string `json:"access_token"`
}

View File

@ -0,0 +1,9 @@
package benefactoreparam
type RefreshAccessRequest struct {
RefreshToken string `json:"refresh_token"`
}
type RefreshAccessResponse struct {
AccessToken string `json:"access_token"`
}

View File

@ -37,4 +37,6 @@ const (
ErrorMsgAssignReceiverAgentKindBoxStatus = "only ready to return kindboxes can be assigned to a receiver agent" ErrorMsgAssignReceiverAgentKindBoxStatus = "only ready to return kindboxes can be assigned to a receiver agent"
ErrorMsgReturnKindBoxStatus = "only returned kindboxes can be enumerated" ErrorMsgReturnKindBoxStatus = "only returned kindboxes can be enumerated"
ErrorMsgInvalidSerialNumberRange = "invalid serial number range" ErrorMsgInvalidSerialNumberRange = "invalid serial number range"
ErrorMsgInvalidOrExpiredJwt = "invalid or expired jwt"
ErrorMsgInvalidRefreshToken = "invalid refresh token"
) )

View File

@ -83,14 +83,11 @@ func (d *DB) GetAdminByID(ctx context.Context, adminID uint) (entity.Admin, erro
row := stmt.QueryRowContext(ctx, adminID) row := stmt.QueryRowContext(ctx, adminID)
admin, err := scanAdmin(row) admin, err := scanAdmin(row)
if err != nil { if err != nil {
sErr := sql.ErrNoRows if errors.Is(err, sql.ErrNoRows) {
//TODO-errorsas: second argument to errors.As should not be *error return entity.Admin{}, richerror.New(op).WithKind(richerror.KindNotFound).
//nolint WithMessage(errmsg.ErrorMsgNotFound)
if errors.As(err, &sErr) {
return entity.Admin{}, nil
} }
// TODO - log unexpected error for better observability
return entity.Admin{}, richerror.New(op).WithErr(err). return entity.Admin{}, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected)
} }

View File

@ -26,15 +26,11 @@ func (d *DB) GetAdminByPhoneNumber(ctx context.Context, phoneNumber string) (ent
row := stmt.QueryRowContext(ctx, phoneNumber) row := stmt.QueryRowContext(ctx, phoneNumber)
admin, err := scanAdmin(row) admin, err := scanAdmin(row)
if err != nil { if err != nil {
sErr := sql.ErrNoRows if errors.Is(err, sql.ErrNoRows) {
//TODO-errorsas: second argument to errors.As should not be *error return entity.Admin{}, richerror.New(op).
//nolint
if errors.As(err, &sErr) {
return entity.Admin{}, richerror.New(op).WithErr(sErr).
WithMessage(errmsg.ErrorMsgNotFound).WithKind(richerror.KindNotFound) WithMessage(errmsg.ErrorMsgNotFound).WithKind(richerror.KindNotFound)
} }
// TODO - log unexpected error for better observability
return entity.Admin{}, richerror.New(op).WithErr(err). return entity.Admin{}, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected)
} }

View File

@ -2,142 +2,39 @@ package mysqlbenefactor
import ( import (
"context" "context"
"database/sql"
"errors" "errors"
"time"
"git.gocasts.ir/ebhomengo/niki/entity" "git.gocasts.ir/ebhomengo/niki/entity"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" 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) { func (d *DB) IsExistBenefactorByPhoneNumber(ctx context.Context, phoneNumber string) (bool, entity.Benefactor, error) {
const op = "mysqlbenefactor.IsExistBenefactorByPhoneNumber" const op = "mysqlbenefactor.IsExistBenefactorByPhoneNumber"
query := `select * from benefactors where phone_number = ?` bnf, err := d.GetByPhoneNumber(ctx, phoneNumber)
//nolint
stmt, err := d.conn.PrepareStatement(ctx, mysql.StatementKeyBenefactorIsExistByPhoneNumber, query)
if err != nil { if err != nil {
return false, entity.Benefactor{}, richerror.New(op).WithErr(err). var richErr richerror.RichError
WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) if errors.As(err, &richErr) && richErr.Kind() == richerror.KindNotFound {
}
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) {
return false, entity.Benefactor{}, nil return false, entity.Benefactor{}, nil
} }
// TODO - log unexpected error for better observability return false, entity.Benefactor{}, richerror.New(op).WithErr(err)
return false, entity.Benefactor{}, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected)
} }
return true, Benefactor, nil return true, bnf, nil
} }
func (d *DB) IsExistBenefactorByID(ctx context.Context, id uint) (bool, error) { func (d *DB) IsExistBenefactorByID(ctx context.Context, id uint) (bool, error) {
const op = "mysqlbenefactor.IsExistBenefactorByID" const op = "mysqlbenefactor.IsExistBenefactorByID"
query := `select * from benefactors where id = ?` _, err := d.GetByID(ctx, id)
//nolint
stmt, err := d.conn.PrepareStatement(ctx, mysql.StatementKeyBenefactorIsExistByID, query)
if err != nil { if err != nil {
return false, richerror.New(op).WithErr(err). var richErr richerror.RichError
WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) if errors.As(err, &richErr) && richErr.Kind() == richerror.KindNotFound {
}
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) {
return false, nil return false, nil
} }
// TODO - log unexpected error for better observability return false, richerror.New(op).WithErr(err)
return false, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected)
} }
return true, nil 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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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`;

View File

@ -22,8 +22,8 @@ const (
StatementKeyAdminGetByID StatementKeyAdminGetByID
StatementKeyAdminGetByPhoneNumber StatementKeyAdminGetByPhoneNumber
StatementKeyAdminAgentGetAll StatementKeyAdminAgentGetAll
StatementKeyBenefactorIsExistByID StatementKeyBenefactorGetByID
StatementKeyBenefactorIsExistByPhoneNumber StatementKeyBenefactorGetByPhoneNumber
StatementKeyBenefactorCreate StatementKeyBenefactorCreate
StatementKeyKindBoxAdd StatementKeyKindBoxAdd
StatementKeyKindBoxAssignReceiverAgent StatementKeyKindBoxAssignReceiverAgent

View File

@ -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
}

View File

@ -3,6 +3,7 @@ package adminservice
import ( import (
"context" "context"
"fmt" "fmt"
"git.gocasts.ir/ebhomengo/niki/service/auth"
"git.gocasts.ir/ebhomengo/niki/config" "git.gocasts.ir/ebhomengo/niki/config"
"git.gocasts.ir/ebhomengo/niki/entity" "git.gocasts.ir/ebhomengo/niki/entity"
@ -10,9 +11,10 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
type AuthGenerator interface { type AuthService interface {
CreateAccessToken(admin entity.Authenticable) (string, error) CreateAccessToken(admin entity.Authenticable) (string, error)
CreateRefreshToken(admin entity.Authenticable) (string, error) CreateRefreshToken(admin entity.Authenticable) (string, error)
ParseRefreshToken(refreshToken string) (*auth.Claims, error)
} }
type Repository interface { type Repository interface {
@ -23,11 +25,11 @@ type Repository interface {
type Service struct { type Service struct {
repo Repository repo Repository
auth AuthGenerator auth AuthService
vld validator.Validator 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{ return Service{
repo: repo, repo: repo,
auth: auth, auth: auth,

39
service/auth/create.go Normal file
View File

@ -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
}

42
service/auth/parse.go Normal file
View File

@ -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
}

View File

@ -1,11 +1,7 @@
package auth package auth
import ( import (
"strings"
"time" "time"
"git.gocasts.ir/ebhomengo/niki/entity"
"github.com/golang-jwt/jwt/v4"
) )
type Config struct { type Config struct {
@ -25,54 +21,3 @@ func New(cfg Config) Service {
Config: cfg, 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
}

View File

@ -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
}

View File

@ -12,7 +12,7 @@ import (
func (s Service) SendOtp(ctx context.Context, req benefactoreparam.SendOtpRequest) (benefactoreparam.SendOtpResponse, error) { func (s Service) SendOtp(ctx context.Context, req benefactoreparam.SendOtpRequest) (benefactoreparam.SendOtpResponse, error) {
const op = "benefactorservice.SendOtp" 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) return benefactoreparam.SendOtpResponse{FieldErrors: fieldErrors}, richerror.New(op).WithErr(vErr)
} }

View File

@ -2,6 +2,7 @@ package benefactorservice
import ( import (
"context" "context"
"git.gocasts.ir/ebhomengo/niki/service/auth"
"time" "time"
smscontract "git.gocasts.ir/ebhomengo/niki/contract/sms" smscontract "git.gocasts.ir/ebhomengo/niki/contract/sms"
@ -22,9 +23,10 @@ type Repository interface {
GetByID(ctx context.Context, benefactorID uint) (entity.Benefactor, error) GetByID(ctx context.Context, benefactorID uint) (entity.Benefactor, error)
} }
type AuthGenerator interface { type AuthService interface {
CreateAccessToken(benefactor entity.Authenticable) (string, error) CreateAccessToken(admin entity.Authenticable) (string, error)
CreateRefreshToken(benefactor entity.Authenticable) (string, error) CreateRefreshToken(admin entity.Authenticable) (string, error)
ParseRefreshToken(refreshToken string) (*auth.Claims, error)
} }
type RedisOtp interface { type RedisOtp interface {
@ -38,13 +40,13 @@ type Service struct {
config Config config Config
redisOtp RedisOtp redisOtp RedisOtp
smsAdapter smscontract.SmsAdapter smsAdapter smscontract.SmsAdapter
auth AuthGenerator auth AuthService
repo Repository repo Repository
vld benefactorvalidator.Validator vld benefactorvalidator.Validator
} }
func New(cfg Config, redisOtp RedisOtp, smsAdapter smscontract.SmsAdapter, func New(cfg Config, redisOtp RedisOtp, smsAdapter smscontract.SmsAdapter,
auth AuthGenerator, repo Repository, vld benefactorvalidator.Validator, auth AuthService, repo Repository, vld benefactorvalidator.Validator,
) Service { ) Service {
return Service{ return Service{
config: cfg, config: cfg,

View File

@ -89,7 +89,7 @@ func New(cfg config.Config, db *mysql.DB, rds *redis.Adapter, smsAdapter smscont
BenefactorAuthSvc = auth.New(cfg.BenefactorAuth) BenefactorAuthSvc = auth.New(cfg.BenefactorAuth)
BenefactorReferTimeSvc = benefactorrefertimeservice.New(referTimeRepo) BenefactorReferTimeSvc = benefactorrefertimeservice.New(referTimeRepo)
BenefactorVld = benefactorvalidator.New() BenefactorVld = benefactorvalidator.New(benefactorRepo)
BenefactorSvc = benefactorservice.New(cfg.BenefactorSvc, redisOtp, smsAdapter, BenefactorAuthSvc, benefactorRepo, BenefactorVld) BenefactorSvc = benefactorservice.New(cfg.BenefactorSvc, redisOtp, smsAdapter, BenefactorAuthSvc, benefactorRepo, BenefactorVld)
BenefactorAddressVld = benefactoraddressvalidator.New(BenefactorSvc, addressRepo) BenefactorAddressVld = benefactoraddressvalidator.New(BenefactorSvc, addressRepo)
BenefactorAddressSvc = benefactoraddressservice.New(addressRepo, BenefactorAddressVld) BenefactorAddressSvc = benefactoraddressservice.New(addressRepo, BenefactorAddressVld)

View File

@ -12,7 +12,7 @@ import (
) )
func (v Validator) ValidateLoginWithPhoneNumberRequest(ctx context.Context, req adminserviceparam.LoginWithPhoneNumberRequest) (map[string]string, error) { 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, if err := validation.ValidateStruct(&req,
// TODO - add regex // TODO - add regex
@ -22,7 +22,9 @@ func (v Validator) ValidateLoginWithPhoneNumberRequest(ctx context.Context, req
validation.Field(&req.PhoneNumber, validation.Field(&req.PhoneNumber,
validation.Required, validation.Required,
validation.Match(regexp.MustCompile(phoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid), 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) fieldErrors := make(map[string]string)
vErr := validation.Errors{} vErr := validation.Errors{}

View File

@ -3,6 +3,7 @@ package adminvalidator
import ( import (
"context" "context"
"errors" "errors"
"git.gocasts.ir/ebhomengo/niki/entity"
"testing" "testing"
adminserviceparam "git.gocasts.ir/ebhomengo/niki/param/admin/admin" adminserviceparam "git.gocasts.ir/ebhomengo/niki/param/admin/admin"
@ -23,7 +24,10 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) {
Password: validPassword, 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) fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req)
assert.NoError(t, err) assert.NoError(t, err)
@ -48,7 +52,10 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) {
Password: "", 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) fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req)
assert.Error(t, err) assert.Error(t, err)
@ -74,7 +81,10 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) {
Password: "short", 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) fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req)
assert.Error(t, err) assert.Error(t, err)
@ -88,7 +98,7 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) {
Password: validPassword, 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) fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req)
assert.Error(t, err) assert.Error(t, err)
@ -102,11 +112,28 @@ func TestValidateLoginWithPhoneNumberRequest(t *testing.T) {
Password: validPassword, 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) fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req)
assert.Error(t, err) assert.Error(t, err)
assert.NotNil(t, fieldErrors) assert.NotNil(t, fieldErrors)
assert.Equal(t, errmsg.ErrorMsgSomethingWentWrong, fieldErrors["phone_number"]) 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"])
})
} }

View File

@ -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 package adminvalidator
import ( import (
context "context" context "context"
entity "git.gocasts.ir/ebhomengo/niki/entity"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
) )
@ -135,6 +136,63 @@ func (_c *MockRepository_AdminExistByPhoneNumber_Call) RunAndReturn(run func(con
return _c 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. // 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. // The first argument is typically a *testing.T value.
func NewMockRepository(t interface { func NewMockRepository(t interface {

View File

@ -3,7 +3,6 @@ package adminvalidator
import ( import (
"context" "context"
"fmt" "fmt"
"git.gocasts.ir/ebhomengo/niki/entity" "git.gocasts.ir/ebhomengo/niki/entity"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
@ -23,6 +22,7 @@ const (
type Repository interface { type Repository interface {
AdminExistByPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) AdminExistByPhoneNumber(ctx context.Context, phoneNumber string) (bool, error)
AdminExistByEmail(ctx context.Context, email string) (bool, error) AdminExistByEmail(ctx context.Context, email string) (bool, error)
GetAdminByPhoneNumber(ctx context.Context, phoneNumber string) (entity.Admin, error)
} }
type Validator struct { type Validator struct {
@ -33,6 +33,24 @@ func New(repo Repository) Validator {
return Validator{repo: repo} 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 { func (v Validator) doesAdminExistByPhoneNumber(ctx context.Context) validation.RuleFunc {
return func(value interface{}) error { return func(value interface{}) error {
phoneNumber, ok := value.(string) phoneNumber, ok := value.(string)

View File

@ -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 package adminkindboxvalidator

View File

@ -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 package adminkindboxvalidator

View File

@ -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 package adminkindboxvalidator

View File

@ -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 package adminkindboxvalidator

View File

@ -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 package adminkindboxvalidator

View File

@ -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 package benefactoraddressvalidator
@ -6,6 +6,7 @@ import (
context "context" context "context"
benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
) )

View File

@ -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 package benefactoraddressvalidator

View File

@ -9,7 +9,8 @@ import (
) )
func TestValidator_ValidateLoginRegisterRequest(t *testing.T) { func TestValidator_ValidateLoginRegisterRequest(t *testing.T) {
validator := New() mockRepository := NewMockRepository(t)
validator := New(mockRepository)
validPhoneNumber := "09123456789" validPhoneNumber := "09123456789"
t.Run("Valid request", func(t *testing.T) { t.Run("Valid request", func(t *testing.T) {

View File

@ -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
}

View File

@ -1,6 +1,7 @@
package benefactorvalidator package benefactorvalidator
import ( import (
"context"
"errors" "errors"
"regexp" "regexp"
@ -10,13 +11,15 @@ import (
validation "github.com/go-ozzo/ozzo-validation/v4" 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" const op = "benefactorvalidator.ValidateSendOtpRequest"
if err := validation.ValidateStruct(&req, if err := validation.ValidateStruct(&req,
validation.Field(&req.PhoneNumber, validation.Field(&req.PhoneNumber,
validation.Required, 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) fieldErrors := make(map[string]string)
vErr := validation.Errors{} vErr := validation.Errors{}

View File

@ -1,23 +1,32 @@
package benefactorvalidator package benefactorvalidator
import ( import (
"testing" "context"
"git.gocasts.ir/ebhomengo/niki/entity"
benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing"
) )
func TestValidator_ValidateSendOtpRequest(t *testing.T) { func TestValidator_ValidateSendOtpRequest(t *testing.T) {
validator := New() mockRepository := NewMockRepository(t)
validator := New(mockRepository)
validPhoneNumber := "09123456789" validPhoneNumber := "09123456789"
ctx := context.Background()
t.Run("Valid request", func(t *testing.T) { t.Run("Valid request", func(t *testing.T) {
req := benefactoreparam.SendOtpRequest{ req := benefactoreparam.SendOtpRequest{
PhoneNumber: validPhoneNumber, 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.NoError(t, err)
assert.Nil(t, fieldErrors) assert.Nil(t, fieldErrors)
}) })
@ -27,7 +36,7 @@ func TestValidator_ValidateSendOtpRequest(t *testing.T) {
PhoneNumber: "", PhoneNumber: "",
} }
fieldErrors, err := validator.ValidateSendOtpRequest(req) fieldErrors, err := validator.ValidateSendOtpRequest(ctx, req)
assert.Error(t, err) assert.Error(t, err)
assert.NotNil(t, fieldErrors) assert.NotNil(t, fieldErrors)
assert.Contains(t, fieldErrors, "phone_number") assert.Contains(t, fieldErrors, "phone_number")
@ -38,9 +47,37 @@ func TestValidator_ValidateSendOtpRequest(t *testing.T) {
PhoneNumber: "12345", PhoneNumber: "12345",
} }
fieldErrors, err := validator.ValidateSendOtpRequest(req) fieldErrors, err := validator.ValidateSendOtpRequest(ctx, req)
assert.Error(t, err) assert.Error(t, err)
assert.NotNil(t, fieldErrors) assert.NotNil(t, fieldErrors)
assert.Equal(t, errmsg.ErrorMsgPhoneNumberIsNotValid, fieldErrors["phone_number"]) 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)
})
} }

View File

@ -1,11 +1,51 @@
package benefactorvalidator 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 ( const (
phoneNumberRegex = "^09\\d{9}$" phoneNumberRegex = "^09\\d{9}$"
) )
type Validator struct{} //go:generate mockery --name Repository
type Repository interface {
func New() Validator { GetByPhoneNumber(ctx context.Context, phoneNumber string) (entity.Benefactor, error)
return Validator{} }
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
}
} }

View File

@ -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 package benefactorkindboxvalidator
@ -6,6 +6,7 @@ import (
context "context" context "context"
addressparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/address" addressparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/address"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
) )

View File

@ -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 package benefactorkindboxvalidator
@ -6,6 +6,7 @@ import (
context "context" context "context"
benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
) )

View File

@ -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 package benefactorkindboxvalidator

View File

@ -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 package benefactorkindboxvalidator

View File

@ -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 package benefactorkindboxreqvalidator
@ -6,6 +6,7 @@ import (
context "context" context "context"
addressparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/address" addressparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/address"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
) )

View File

@ -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 package benefactorkindboxreqvalidator
@ -6,6 +6,7 @@ import (
context "context" context "context"
benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor" benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
) )

View File

@ -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 package benefactorkindboxreqvalidator

View File

@ -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 package benefactorkindboxreqvalidator