forked from ebhomengo/niki
1
0
Fork 0

Merge branch 'develop' into agent-structure-refactor

This commit is contained in:
hossein 2024-09-16 04:49:41 +00:00
commit 6819dde2ce
58 changed files with 1123 additions and 254 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("/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"
}

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("/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
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
}

View File

@ -1013,6 +1013,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.",
@ -2960,7 +3056,11 @@ const docTemplate = `{
},
"deliver_refer_date": {
"type": "string",
"example": "2025-01-02 15:04:05"
"example": "2025-01-02T15:04:05Z"
},
"deliver_refer_time_id": {
"type": "integer",
"example": 1
},
"kind_box_type": {
"allOf": [
@ -3153,6 +3253,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": {
@ -3486,6 +3602,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": {

View File

@ -1002,6 +1002,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.",
@ -2949,7 +3045,11 @@
},
"deliver_refer_date": {
"type": "string",
"example": "2025-01-02 15:04:05"
"example": "2025-01-02T15:04:05Z"
},
"deliver_refer_time_id": {
"type": "integer",
"example": 1
},
"kind_box_type": {
"allOf": [
@ -3142,6 +3242,22 @@
}
}
},
"adminserviceparam.RefreshAccessRequest": {
"type": "object",
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"adminserviceparam.RefreshAccessResponse": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
}
}
},
"adminserviceparam.RegisterRequest": {
"type": "object",
"properties": {
@ -3475,6 +3591,22 @@
}
}
},
"benefactoreparam.RefreshAccessRequest": {
"type": "object",
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"benefactoreparam.RefreshAccessResponse": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
}
}
},
"benefactoreparam.SendOtpRequest": {
"type": "object",
"properties": {

View File

@ -287,8 +287,11 @@ definitions:
example: 1
type: integer
deliver_refer_date:
example: "2025-01-02 15:04:05"
example: "2025-01-02T15:04:05Z"
type: string
deliver_refer_time_id:
example: 1
type: integer
kind_box_type:
allOf:
- $ref: '#/definitions/entity.KindBoxType'
@ -412,6 +415,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:
@ -628,6 +641,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:
@ -1598,6 +1621,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:
@ -2571,6 +2627,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:

View File

@ -12,4 +12,5 @@ type Benefactor struct {
Gender Gender
BirthDate time.Time
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

@ -1,12 +1,16 @@
package adminkindboxreqparam
import entity "git.gocasts.ir/ebhomengo/niki/entity"
import (
entity "git.gocasts.ir/ebhomengo/niki/entity"
"time"
)
type KindBoxReqAddRequest struct {
BenefactorID uint `json:"benefactor_id" example:"1"`
KindBoxType entity.KindBoxType `json:"kind_box_type" example:"on-table"`
DeliverAddressID uint `json:"deliver_address_id" example:"1"`
DeliverReferDate string `json:"deliver_refer_date" example:"2025-01-02 15:04:05"`
DeliverReferDate time.Time `json:"deliver_refer_date" example:"2025-01-02T15:04:05Z"`
DeliverReferTimeID uint `json:"deliver_refer_time_id" example:"1"`
CountRequested uint `json:"count_requested" example:"2"`
}

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"
ErrorMsgReturnKindBoxStatus = "only returned kindboxes can be enumerated"
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)
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)
}

View File

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

View File

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

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

@ -2,7 +2,6 @@ package mysqlkindboxreq
import (
"context"
"git.gocasts.ir/ebhomengo/niki/entity"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
@ -26,7 +25,7 @@ func (d *DB) AddKindBoxReq(ctx context.Context, kindBoxReq entity.KindBoxReq) (e
kindBoxReq.DeliverReferTimeID, kindBoxReq.Status)
if err != nil {
return entity.KindBoxReq{}, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgNotFound).WithKind(richerror.KindUnexpected)
WithKind(richerror.KindUnexpected)
}
//nolint

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
StatementKeyAdminGetByPhoneNumber
StatementKeyAdminAgentGetAll
StatementKeyBenefactorIsExistByID
StatementKeyBenefactorIsExistByPhoneNumber
StatementKeyBenefactorGetByID
StatementKeyBenefactorGetByPhoneNumber
StatementKeyBenefactorCreate
StatementKeyKindBoxAdd
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 (
"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,

View File

@ -2,8 +2,6 @@ package adminkindboxreqservice
import (
"context"
"time"
"git.gocasts.ir/ebhomengo/niki/entity"
param "git.gocasts.ir/ebhomengo/niki/param/admin/kind_box_req"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
@ -14,15 +12,12 @@ func (s Service) Add(ctx context.Context, req param.KindBoxReqAddRequest) (param
if fieldErrors, vErr := s.vld.ValidateAddRequest(ctx, req); vErr != nil {
return param.KindBoxReqAddResponse{FieldErrors: fieldErrors}, richerror.New(op).WithErr(vErr)
}
date, tErr := time.Parse(time.DateTime, req.DeliverReferDate)
if tErr != nil {
return param.KindBoxReqAddResponse{}, richerror.New(op).WithErr(tErr).WithKind(richerror.KindInvalid)
}
kindBoxReq, err := s.repo.AddKindBoxReq(ctx, entity.KindBoxReq{
BenefactorID: req.BenefactorID,
KindBoxType: req.KindBoxType,
DeliverAddressID: req.DeliverAddressID,
DeliverReferDate: date,
DeliverReferDate: req.DeliverReferDate,
DeliverReferTimeID: req.DeliverReferTimeID,
CountRequested: req.CountRequested,
Status: entity.KindBoxReqPendingStatus,
})

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

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

View File

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

View File

@ -94,7 +94,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)

View File

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

View File

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

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
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 {

View File

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

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

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

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

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

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

View File

@ -38,6 +38,11 @@ func (v Validator) ValidateAddRequest(ctx context.Context, req param.KindBoxReqA
validation.Required,
validation.By(v.isDateValid),
),
validation.Field(&req.DeliverReferTimeID,
validation.Required,
validation.By(v.isReferTimeIDValid(ctx)),
),
); err != nil {
fieldErrors := make(map[string]string)
@ -112,6 +117,7 @@ func (v Validator) isDateValid(value interface{}) error {
if !ok {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
if date.Before(time.Now()) {
return fmt.Errorf(errmsg.ErrorMsgInvalidInput)
}

View File

@ -25,8 +25,8 @@ func (v Validator) ValidateUpdateRequest(ctx context.Context, req param.KindBoxR
validation.Max(uint(MaxKindBoxReq)),
),
validation.Field(&req.CountAccepted,
validation.Min(MinKindBoxReq),
validation.Max(MaxKindBoxReq),
validation.Min(uint(MinKindBoxReq)),
validation.Max(uint(MaxKindBoxReq)),
validation.When(req.CountRequested > 0, validation.Max(req.CountRequested)),
validation.By(v.checkCountAcceptedMustBeLessThanCountRequested(ctx, req.ID)),
),

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
@ -6,6 +6,7 @@ import (
context "context"
benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor"
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

View File

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

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

View File

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

View File

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

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
@ -6,6 +6,7 @@ import (
context "context"
addressparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/address"
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
@ -6,6 +6,7 @@ import (
context "context"
benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor"
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

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

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
@ -6,6 +6,7 @@ import (
context "context"
addressparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/address"
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
@ -6,6 +6,7 @@ import (
context "context"
benefactoreparam "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactor"
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

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