feat(service): add admin login and register

This commit is contained in:
Masood Keshvari 2024-01-19 20:26:11 +03:30 committed by Alireza Mokhtari Garakani
parent 085989538d
commit 6e0d616036
33 changed files with 847 additions and 45 deletions

View File

@ -32,6 +32,8 @@ kavenegar_sms_provider:
otp_template_new_user: ebhomeverify
otp_template_registered_user: ebhomeverify
admin_auth:
sign_key: admin-jwt_secret_test_nik

View File

@ -4,7 +4,8 @@ import (
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
smsprovider "git.gocasts.ir/ebhomengo/niki/adapter/sms_provider/kavenegar"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
authservice "git.gocasts.ir/ebhomengo/niki/service/auth/benefactor"
adminauthservice "git.gocasts.ir/ebhomengo/niki/service/auth/admin"
benefactorauthservice "git.gocasts.ir/ebhomengo/niki/service/auth/benefactor"
benefactorservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/benefactor"
)
@ -15,7 +16,8 @@ type HTTPServer struct {
type Config struct {
HTTPServer HTTPServer `koanf:"http_server"`
Mysql mysql.Config `koanf:"mysql"`
Auth authservice.Config `koanf:"auth"`
Auth benefactorauthservice.Config `koanf:"auth"`
AdminAuth adminauthservice.Config `koanf:"admin_auth"`
Redis redis.Config `koanf:"redis"`
KavenegarSmsProvider smsprovider.Config `koanf:"kavenegar_sms_provider"`
BenefactorSvc benefactorservice.Config `koanf:"benefactor_service"`

View File

@ -12,4 +12,5 @@ const (
AccessTokenExpireDuration = time.Hour * 24
RefreshTokenExpireDuration = time.Hour * 24 * 7
AuthMiddlewareContextKey = "claims"
BcryptCost = 3
)

View File

@ -0,0 +1,25 @@
package adminhandler
import (
adminservice "git.gocasts.ir/ebhomengo/niki/service/admin/admin"
adminauthservice "git.gocasts.ir/ebhomengo/niki/service/auth/admin"
adminvalidator "git.gocasts.ir/ebhomengo/niki/validator/admin/admin"
)
type Handler struct {
authConfig adminauthservice.Config
authSvc adminauthservice.Service
adminSvc adminservice.Service
adminVld adminvalidator.Validator
}
func New(authConfig adminauthservice.Config, authSvc adminauthservice.Service,
adminSvc adminservice.Service, adminVld adminvalidator.Validator,
) Handler {
return Handler{
authConfig: authConfig,
authSvc: authSvc,
adminSvc: adminSvc,
adminVld: adminVld,
}
}

View File

@ -0,0 +1,33 @@
package adminhandler
import (
adminserviceparam "git.gocasts.ir/ebhomengo/niki/param/admin/admin"
httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg"
"github.com/labstack/echo/v4"
"net/http"
)
func (h Handler) LoginByPhoneNumber(c echo.Context) error {
var req adminserviceparam.LoginWithPhoneNumberRequest
if bErr := c.Bind(&req); bErr != nil {
return echo.NewHTTPError(http.StatusBadRequest)
}
if fieldErrors, err := h.adminVld.ValidateLoginWithPhoneNumberRequest(req); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, echo.Map{
"message": msg,
"errors": fieldErrors,
})
}
resp, sErr := h.adminSvc.LoginWithPhoneNumber(c.Request().Context(), req)
if sErr != nil {
msg, code := httpmsg.Error(sErr)
return echo.NewHTTPError(code, msg)
}
return c.JSON(http.StatusOK, resp)
}

View File

@ -0,0 +1,33 @@
package adminhandler
import (
adminserviceparam "git.gocasts.ir/ebhomengo/niki/param/admin/admin"
httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg"
"github.com/labstack/echo/v4"
"net/http"
)
func (h Handler) Register(c echo.Context) error {
var req adminserviceparam.RegisterRequest
if bErr := c.Bind(&req); bErr != nil {
return echo.NewHTTPError(http.StatusBadRequest)
}
if fieldErrors, err := h.adminVld.ValidateRegisterRequest(req); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, echo.Map{
"message": msg,
"errors": fieldErrors,
})
}
resp, sErr := h.adminSvc.Register(c.Request().Context(), req)
if sErr != nil {
msg, code := httpmsg.Error(sErr)
return echo.NewHTTPError(code, msg)
}
return c.JSON(http.StatusOK, resp)
}

View File

@ -0,0 +1,14 @@
package adminhandler
import "github.com/labstack/echo/v4"
func (h Handler) SetRoutes(e *echo.Echo) {
r := e.Group("/admins")
//nolint:gocritic
//r.POST("/", h.Add).Name = "admin-addkindboxreq"
r.POST("/register", h.Register)
r.POST("/login-by-phone", h.LoginByPhoneNumber)
//nolint:gocritic
//r.PATCH("/:id", h.Update).Name = "admin-updatekindboxreq"
}

View File

@ -2,6 +2,10 @@ package httpserver
import (
"fmt"
adminhandler "git.gocasts.ir/ebhomengo/niki/delivery/http_server/admin/admin"
adminservice "git.gocasts.ir/ebhomengo/niki/service/admin/admin"
adminauthservice "git.gocasts.ir/ebhomengo/niki/service/auth/admin"
adminvalidator "git.gocasts.ir/ebhomengo/niki/validator/admin/admin"
config "git.gocasts.ir/ebhomengo/niki/config"
benefactorbasehandler "git.gocasts.ir/ebhomengo/niki/delivery/http_server/benefactor/base"
@ -23,6 +27,7 @@ type Server struct {
benefactorHandler benefactorhandler.Handler
benefactorKindBoxReqHandler benefactorkindboxreqhandler.Handler
benefactorBaseHandler benefactorbasehandler.Handler
adminHandler adminhandler.Handler
}
func New(
@ -33,6 +38,9 @@ func New(
benefactorKindBoxReqSvc benefactorkindboxreqservice.Service,
benefactorKindBoxReqVld benefactorkindboxreqvalidator.Validator,
benefactorAddressSvc benefactoraddressservice.Service,
adminSvc adminservice.Service,
adminVld adminvalidator.Validator,
adminAuthSvc adminauthservice.Service,
) Server {
return Server{
Router: echo.New(),
@ -40,6 +48,7 @@ func New(
benefactorHandler: benefactorhandler.New(cfg.Auth, authSvc, benefactorSvc, benefactorVld, benefactorAddressSvc),
benefactorKindBoxReqHandler: benefactorkindboxreqhandler.New(cfg.Auth, authSvc, benefactorKindBoxReqSvc, benefactorKindBoxReqVld),
benefactorBaseHandler: benefactorbasehandler.New(benefactorAddressSvc),
adminHandler: adminhandler.New(cfg.AdminAuth, adminAuthSvc, adminSvc, adminVld),
}
}
@ -53,6 +62,7 @@ func (s Server) Serve() {
s.benefactorHandler.SetRoutes(s.Router)
s.benefactorKindBoxReqHandler.SetRoutes(s.Router)
s.benefactorBaseHandler.SetRoutes(s.Router)
s.adminHandler.SetRoutes(s.Router)
// Start server
address := fmt.Sprintf(":%d", s.config.HTTPServer.Port)

View File

@ -1,19 +1,23 @@
package entity
import "time"
type Admin struct {
ID uint
FirstName string
LastName string
password string
PhoneNumber string
Role AdminRole
Address string
Description string
Email string
City string
Gender Gender
Status AdminStatus
Birthday time.Time
StatusChangedAt time.Time
}
func (a *Admin) GetPassword() string {
return a.password
}
func (a *Admin) SetPassword(password string) {
a.password = password
}

View File

@ -16,6 +16,10 @@ func (s AdminRole) String() string {
return AdminRoleStrings[s]
}
func (s AdminRole) IsValid() bool {
return s > 0 && int(s) <= len(AdminRoleStrings)
}
// AllAdminRole returns a slice containing all string values of AdminRole.
func AllAdminRole() []string {
roleStrings := make([]string, len(AdminRoleStrings))

View File

@ -16,6 +16,10 @@ func (s AdminStatus) String() string {
return AdminStatusStrings[s]
}
func (s AdminStatus) IsValid() bool {
return s > 0 && int(s) <= len(AdminStatusStrings)
}
// AllAdminStatus returns a slice containing all string values of AdminStatus.
func AllAdminStatus() []string {
statusStrings := make([]string, len(AdminStatusStrings))

View File

@ -26,6 +26,10 @@ func AllGender() []string {
return statusStrings
}
func (s Gender) IsValid() bool {
return s > 0 && int(s) <= len(GenderStrings)
}
// MapToGender converts a string to the corresponding Gender value.
func MapToGender(statusStr string) Gender {
for status, str := range GenderStrings {

17
main.go
View File

@ -9,13 +9,17 @@ import (
"git.gocasts.ir/ebhomengo/niki/repository/migrator"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
mysqladdress "git.gocasts.ir/ebhomengo/niki/repository/mysql/address"
mysqladmin "git.gocasts.ir/ebhomengo/niki/repository/mysql/admin"
mysqlbenefactor "git.gocasts.ir/ebhomengo/niki/repository/mysql/benefactor"
mysqlkindboxreq "git.gocasts.ir/ebhomengo/niki/repository/mysql/kind_box_req"
redisotp "git.gocasts.ir/ebhomengo/niki/repository/redis/redis_otp"
adminservice "git.gocasts.ir/ebhomengo/niki/service/admin/admin"
adminauthservice "git.gocasts.ir/ebhomengo/niki/service/auth/admin"
authservice "git.gocasts.ir/ebhomengo/niki/service/auth/benefactor"
benefactoraddressservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/address"
benefactorservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/benefactor"
benefactorkindboxreqservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/kind_box_req"
adminvalidator "git.gocasts.ir/ebhomengo/niki/validator/admin/admin"
benefactorvalidator "git.gocasts.ir/ebhomengo/niki/validator/benefactor/benefactor"
benefactorkindboxreqvalidator "git.gocasts.ir/ebhomengo/niki/validator/benefactor/kind_box_req"
_ "github.com/go-sql-driver/mysql"
@ -27,8 +31,10 @@ func main() {
mgr := migrator.New(cfg.Mysql)
mgr.Up()
authSvc, benefactorSvc, benefactorVld, benefactorKindBoxReqSvc, benefactorKindBoxReqVld, benefactorAddressSvc := setupServices(cfg)
server := httpserver.New(cfg, benefactorSvc, benefactorVld, authSvc, benefactorKindBoxReqSvc, benefactorKindBoxReqVld, benefactorAddressSvc)
authSvc, benefactorSvc, benefactorVld, benefactorKindBoxReqSvc, benefactorKindBoxReqVld, benefactorAddressSvc,
adminSvc, adminVld, adminAuthSvc := setupServices(cfg)
server := httpserver.New(cfg, benefactorSvc, benefactorVld, authSvc, benefactorKindBoxReqSvc, benefactorKindBoxReqVld,
benefactorAddressSvc, adminSvc, adminVld, adminAuthSvc)
server.Serve()
}
@ -36,7 +42,7 @@ func main() {
func setupServices(cfg config.Config) (
authSvc authservice.Service, benefactorSvc benefactorservice.Service, benefactorVld benefactorvalidator.Validator,
benefactorKindBoxReqSvc benefactorkindboxreqservice.Service, benefactorKindBoxReqVld benefactorkindboxreqvalidator.Validator,
benefactorAddressSvc benefactoraddressservice.Service,
benefactorAddressSvc benefactoraddressservice.Service, adminSvc adminservice.Service, adminVld adminvalidator.Validator, adminAuthSvc adminauthservice.Service,
) {
authSvc = authservice.New(cfg.Auth)
@ -58,5 +64,10 @@ func setupServices(cfg config.Config) (
benefactorKindBoxReqSvc = benefactorkindboxreqservice.New(benefactorKindBoxReqMysql)
benefactorKindBoxReqVld = benefactorkindboxreqvalidator.New(benefactorKindBoxReqMysql, benefactorSvc, benefactorAddressSvc)
adminAuthSvc = adminauthservice.New(cfg.AdminAuth)
adminMysql := mysqladmin.New(MysqlRepo)
adminVld = adminvalidator.New(adminMysql)
adminSvc = adminservice.New(adminMysql, adminAuthSvc)
return
}

View File

@ -0,0 +1,13 @@
package adminserviceparam
import "git.gocasts.ir/ebhomengo/niki/entity"
type LoginWithPhoneNumberRequest struct {
PhoneNumber string `json:"phone_number"`
Password string `json:"password"`
}
type LoginWithPhoneNumberResponse struct {
Admin entity.Admin `json:"admin"`
Tokens Tokens `json:"tokens"`
}

View File

@ -0,0 +1,19 @@
package adminserviceparam
import "git.gocasts.ir/ebhomengo/niki/entity"
type RegisterRequest struct {
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
Password *string `json:"password"`
PhoneNumber *string `json:"phone_number"`
Role *entity.AdminRole `json:"role"`
Description *string `json:"description"`
Email *string `json:"email"`
Gender *entity.Gender `json:"gender"`
Status *entity.AdminStatus `json:"status"`
}
type RegisterResponse struct {
Admin entity.Admin
}

View File

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

View File

@ -6,10 +6,12 @@ const (
ErrorMsgInvalidInput = "invalid input"
ErrorMsgInvalidStatus = "invalid status"
ErrorMsgPhoneNumberIsNotUnique = "phone number is not unique"
ErrorMsgEmailIsNotUnique = "email is not unique"
ErrorMsgPhoneNumberIsNotValid = "phone number is not valid"
ErrorMsgUserNotAllowed = "user not allowed"
ErrorMsgUserNotFound = "benefactor not found"
ErrorMsgOtpCodeExist = "please wait a little bit"
ErrorMsgOtpCodeIsNotValid = "verification code is not valid"
ErrorMsgCantScanQueryResult = "can't scan query result"
ErrorMsgPhoneNumberOrPassIsIncorrect = "phone number or password is incorrect"
)

View File

@ -0,0 +1,11 @@
package mysqladmin
import "git.gocasts.ir/ebhomengo/niki/repository/mysql"
type DB struct {
conn *mysql.DB
}
func New(conn *mysql.DB) *DB {
return &DB{conn: conn}
}

View File

@ -0,0 +1,28 @@
package mysqladmin
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"
)
func (d DB) AddAdmin(ctx context.Context, admin entity.Admin) (entity.Admin, error) {
const op = "mysqladmin.AddAdmin"
res, err := d.conn.Conn().ExecContext(ctx, `insert into admins(first_name,last_name,password,phone_number,
role,description,email,gender,status) values (?,?,?,?,?,?,?,?,?)`,
admin.FirstName, admin.LastName, admin.GetPassword(), admin.PhoneNumber, admin.Role.String(), admin.Description, admin.Email,
admin.Gender.String(), admin.Status.String())
if err != nil {
return entity.Admin{}, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgNotFound).WithKind(richerror.KindUnexpected)
}
//nolint
// err is always nil
id, _ := res.LastInsertId()
admin.ID = uint(id)
return admin, nil
}

View File

@ -0,0 +1,53 @@
package mysqladmin
import (
"context"
"database/sql"
"errors"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
func (d DB) AdminExistByPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) {
const op = "mysqlbenefactor.IsExistBenefactorByID"
row := d.conn.Conn().QueryRowContext(ctx, `select * from admins where phone_number = ?`, phoneNumber)
_, 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 false, nil
}
// TODO - log unexpected error for better observability
return false, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected)
}
return true, nil
}
func (d DB) AdminExistByEmail(ctx context.Context, email string) (bool, error) {
const op = "mysqlbenefactor.IsExistBenefactorByID"
row := d.conn.Conn().QueryRowContext(ctx, `select * from admins where email = ?`, email)
_, 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 false, nil
}
// TODO - log unexpected error for better observability
return false, richerror.New(op).WithErr(err).
WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected)
}
return true, nil
}

View File

@ -0,0 +1,84 @@
package mysqladmin
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"
"time"
)
func (d DB) GetAdminByPhoneNumber(ctx context.Context, phoneNumber string) (entity.Admin, error) {
const op = "mysqlbenefactor.IsExistBenefactorByID"
row := d.conn.Conn().QueryRowContext(ctx, `select * from admins where phone_number = ?`, 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).
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)
}
return admin, nil
}
func scanAdmin(scanner mysql.Scanner) (entity.Admin, error) {
var createdAt time.Time
var admin entity.Admin
var roleStr, statusStr, password string
// TODO - use db model and mapper between entity and db model OR use this approach
var adminNullableFields nullableFields
err := scanner.Scan(&admin.ID, &adminNullableFields.firstName,
&adminNullableFields.lastName, &password, &admin.PhoneNumber,
&roleStr, &adminNullableFields.description,
&adminNullableFields.email, &adminNullableFields.genderStr,
&statusStr, &createdAt)
admin.Role = entity.MapToAdminRole(roleStr)
admin.Status = entity.MapToAdminStatus(statusStr)
admin.SetPassword(password)
mapNotNullToAdmin(adminNullableFields, &admin)
return admin, err
}
type nullableFields struct {
firstName sql.NullString
lastName sql.NullString
description sql.NullString
email sql.NullString
genderStr sql.NullString
}
// TODO - find the other solution.
func mapNotNullToAdmin(data nullableFields, admin *entity.Admin) {
if data.firstName.Valid {
admin.FirstName = data.firstName.String
}
if data.lastName.Valid {
admin.LastName = data.lastName.String
}
if data.description.Valid {
admin.Description = data.description.String
}
if data.email.Valid {
admin.Email = data.email.String
}
if data.genderStr.Valid {
admin.Gender = entity.MapToGender(data.genderStr.String)
}
}

View File

@ -0,0 +1,18 @@
-- +migrate Up
CREATE TABLE `admins`
(
`id` INT PRIMARY KEY AUTO_INCREMENT,
`first_name` VARCHAR(191),
`last_name` VARCHAR(191),
`password` TEXT NOT NULL,
`phone_number` VARCHAR(191) NOT NULL UNIQUE,
`role` ENUM('super-admin','admin') NOT NULL,
`description` TEXT,
`email` VARCHAR(191) NOT NULL UNIQUE,
`gender` VARCHAR(191),
`status` VARCHAR(191),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- +migrate Down
DROP TABLE `admins`;

View File

@ -0,0 +1,9 @@
-- +migrate Up
-- what can we do for password?
INSERT INTO `admins` (`id`, `phone_number`, `email`,`password`,`role`,`status`)
VALUES (1, '09122702856', 'keshvari@gmail.com','Abc123456','super-admin','active');
-- +migrate Down
DELETE
FROM `admins`
WHERE id '1' ;

View File

@ -0,0 +1,40 @@
package adminservice
import (
"context"
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) LoginWithPhoneNumber(ctx context.Context, req adminserviceparam.LoginWithPhoneNumberRequest) (adminserviceparam.LoginWithPhoneNumberResponse, error) {
const op = richerror.Op("adminservice.LoginWithPhoneNumber")
admin, err := s.repo.GetAdminByPhoneNumber(ctx, req.PhoneNumber)
if err != nil {
return adminserviceparam.LoginWithPhoneNumberResponse{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected)
}
if cErr := CompareHash(admin.GetPassword(), req.Password); cErr != nil {
return adminserviceparam.LoginWithPhoneNumberResponse{}, richerror.New(op).WithErr(cErr).WithMessage(errmsg.ErrorMsgPhoneNumberOrPassIsIncorrect).WithKind(richerror.KindForbidden)
}
accessToken, aErr := s.auth.CreateAccessToken(admin)
if aErr != nil {
return adminserviceparam.LoginWithPhoneNumberResponse{}, richerror.New(op).WithErr(aErr).WithKind(richerror.KindUnexpected)
}
refreshToken, rErr := s.auth.CreateRefreshToken(admin)
if rErr != nil {
return adminserviceparam.LoginWithPhoneNumberResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected)
}
return adminserviceparam.LoginWithPhoneNumberResponse{
Admin: admin,
Tokens: adminserviceparam.Tokens{
AccessToken: accessToken,
RefreshToken: refreshToken,
},
}, nil
}

View File

@ -0,0 +1,53 @@
package adminservice
import (
"context"
"git.gocasts.ir/ebhomengo/niki/entity"
adminserviceparam "git.gocasts.ir/ebhomengo/niki/param/admin/admin"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
func (s Service) Register(ctx context.Context, req adminserviceparam.RegisterRequest) (adminserviceparam.RegisterResponse, error) {
const op = richerror.Op("adminservice.Register")
var newAdmin entity.Admin
if req.FirstName != nil {
newAdmin.FirstName = *req.FirstName
}
if req.LastName != nil {
newAdmin.LastName = *req.LastName
}
if req.PhoneNumber != nil {
newAdmin.PhoneNumber = *req.PhoneNumber
}
if req.Role != nil {
newAdmin.Role = *req.Role
}
if req.Description != nil {
newAdmin.LastName = *req.Description
}
if req.Email != nil {
newAdmin.Email = *req.Email
}
if req.Gender != nil {
newAdmin.Gender = *req.Gender
}
if req.Description != nil {
newAdmin.LastName = *req.Description
}
if req.Email != nil {
newAdmin.Status = *req.Status
}
if bErr := GenerateHash(req.Password); bErr != nil {
return adminserviceparam.RegisterResponse{}, richerror.New(op).WithErr(bErr).WithKind(richerror.KindUnexpected)
}
newAdmin.SetPassword(*req.Password)
admin, err := s.repo.AddAdmin(ctx, newAdmin)
if err != nil {
return adminserviceparam.RegisterResponse{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected)
}
return adminserviceparam.RegisterResponse{Admin: admin}, err
}

View File

@ -0,0 +1,44 @@
package adminservice
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/config"
"git.gocasts.ir/ebhomengo/niki/entity"
"golang.org/x/crypto/bcrypt"
)
type AuthGenerator interface {
CreateAccessToken(benefactor entity.Admin) (string, error)
CreateRefreshToken(benefactor entity.Admin) (string, error)
}
type Repository interface {
AddAdmin(ctx context.Context, admin entity.Admin) (entity.Admin, error)
GetAdminByPhoneNumber(ctx context.Context, phoneNumber string) (entity.Admin, error)
}
type Service struct {
repo Repository
auth AuthGenerator
}
func New(repo Repository, auth AuthGenerator) Service {
return Service{
repo: repo,
auth: auth,
}
}
func GenerateHash(password *string) error {
hashedPassword, bErr := bcrypt.GenerateFromPassword([]byte(*password), config.BcryptCost)
if bErr != nil {
return fmt.Errorf("bcrypt error: %w", bErr)
}
*password = string(hashedPassword)
return nil
}
func CompareHash(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

View File

@ -0,0 +1,16 @@
package adminauthservice
import (
"git.gocasts.ir/ebhomengo/niki/entity"
"github.com/golang-jwt/jwt/v4"
)
type Claims struct {
jwt.RegisteredClaims
UserID uint `json:"user_id"`
Role entity.AdminRole `json:"role"`
}
func (c Claims) Valid() error {
return c.RegisteredClaims.Valid()
}

View File

@ -1 +0,0 @@
package admin

View File

@ -1,6 +1,9 @@
package admin
package adminauthservice
import (
"git.gocasts.ir/ebhomengo/niki/entity"
"github.com/golang-jwt/jwt/v4"
"strings"
"time"
)
@ -21,3 +24,54 @@ func New(cfg Config) Service {
config: cfg,
}
}
func (s Service) CreateAccessToken(admin entity.Admin) (string, error) {
return s.createToken(admin.ID, admin.Role, s.config.AccessSubject, s.config.AccessExpirationTime)
}
func (s Service) CreateRefreshToken(admin entity.Admin) (string, error) {
return s.createToken(admin.ID, admin.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 entity.AdminRole, 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

@ -22,7 +22,7 @@ func (s Service) LoginOrRegister(ctx context.Context, req benefactoreparam.Login
_, dErr := s.redisOtp.DeleteCodeByPhoneNumber(ctx, req.PhoneNumber)
if dErr != nil {
return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(gErr).WithKind(richerror.KindUnexpected)
return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(dErr).WithKind(richerror.KindUnexpected)
}
isExist, benefactor, rErr := s.repo.IsExistBenefactorByPhoneNumber(ctx, req.PhoneNumber)
@ -36,19 +36,19 @@ func (s Service) LoginOrRegister(ctx context.Context, req benefactoreparam.Login
Role: entity.UserBenefactorRole,
})
if err != nil {
return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected)
return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected)
}
benefactor = newBenefactor
}
accessToken, err := s.auth.CreateAccessToken(benefactor)
if err != nil {
return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected)
accessToken, aErr := s.auth.CreateAccessToken(benefactor)
if aErr != nil {
return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(aErr).WithKind(richerror.KindUnexpected)
}
refreshToken, err := s.auth.CreateRefreshToken(benefactor)
if err != nil {
refreshToken, rErr := s.auth.CreateRefreshToken(benefactor)
if rErr != nil {
return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected)
}

View File

@ -0,0 +1,42 @@
package adminvalidator
import (
"errors"
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"
validation "github.com/go-ozzo/ozzo-validation/v4"
"regexp"
)
func (v Validator) ValidateLoginWithPhoneNumberRequest(req adminserviceparam.LoginWithPhoneNumberRequest) (map[string]string, error) {
const op = "adminvalidator.ValidateRegisterRequest"
if err := validation.ValidateStruct(&req,
//TODO - add regex
validation.Field(&req.Password, validation.Required, validation.NotNil,
validation.Length(8, 0)),
validation.Field(&req.PhoneNumber,
validation.Required,
validation.Match(regexp.MustCompile(phoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid),
validation.By(v.doesAdminExistByPhoneNumber))); err != nil {
fieldErrors := make(map[string]string)
vErr := validation.Errors{}
if errors.As(err, &vErr) {
for key, value := range vErr {
if value != nil {
fieldErrors[key] = value.Error()
}
}
}
return fieldErrors, richerror.New(op).WithMessage(errmsg.ErrorMsgInvalidInput).
WithKind(richerror.KindInvalid).
WithMeta(map[string]interface{}{"req": req}).WithErr(err)
}
//nolint
return nil, nil
}

View File

@ -0,0 +1,53 @@
package adminvalidator
import (
"errors"
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"
"github.com/go-ozzo/ozzo-validation/is"
validation "github.com/go-ozzo/ozzo-validation/v4"
"regexp"
)
func (v Validator) ValidateRegisterRequest(req adminserviceparam.RegisterRequest) (map[string]string, error) {
const op = "adminvalidator.ValidateRegisterRequest"
if err := validation.ValidateStruct(&req,
// TODO - add length of code config from benefactor config
validation.Field(&req.FirstName,
validation.Length(3, 40)),
validation.Field(&req.LastName,
validation.Length(3, 40)),
//TODO - add regex
validation.Field(&req.Password, validation.Required, validation.NotNil,
validation.Length(8, 0)),
validation.Field(&req.Gender, validation.By(v.IsGenderValid)),
validation.Field(&req.Role, validation.By(v.IsRoleValid), validation.Required),
validation.Field(&req.Status, validation.By(v.IsStatusValid), validation.Required),
validation.Field(&req.Email, validation.Required, is.Email,
validation.By(v.doesAdminExistByEmail)),
validation.Field(&req.PhoneNumber,
validation.Required,
validation.Match(regexp.MustCompile(phoneNumberRegex)).Error(errmsg.ErrorMsgPhoneNumberIsNotValid),
validation.By(v.IsPhoneNumberUnique))); err != nil {
fieldErrors := make(map[string]string)
vErr := validation.Errors{}
if errors.As(err, &vErr) {
for key, value := range vErr {
if value != nil {
fieldErrors[key] = value.Error()
}
}
}
return fieldErrors, richerror.New(op).WithMessage(errmsg.ErrorMsgInvalidInput).
WithKind(richerror.KindInvalid).
WithMeta(map[string]interface{}{"req": req}).WithErr(err)
}
//nolint
return nil, nil
}

View File

@ -0,0 +1,111 @@
package adminvalidator
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/entity"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
)
const (
phoneNumberRegex = "^09\\d{9}$"
)
type Repository interface {
AdminExistByPhoneNumber(ctx context.Context, phoneNumber string) (bool, error)
AdminExistByEmail(ctx context.Context, email string) (bool, error)
}
type Validator struct {
repo Repository
}
func New(repo Repository) Validator {
return Validator{repo: repo}
}
func (v Validator) doesAdminExistByPhoneNumber(value interface{}) error {
phoneNumber, ok := value.(string)
if !ok {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
adminExisted, err := v.repo.AdminExistByPhoneNumber(context.Background(), phoneNumber)
if err != nil {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
if !adminExisted {
return fmt.Errorf(errmsg.ErrorMsgPhoneNumberOrPassIsIncorrect)
}
return nil
}
func (v Validator) IsPhoneNumberUnique(value interface{}) error {
phoneNumber, ok := value.(*string)
if !ok {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
adminExisted, err := v.repo.AdminExistByPhoneNumber(context.Background(), *phoneNumber)
if err != nil {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
if adminExisted {
return fmt.Errorf(errmsg.ErrorMsgPhoneNumberIsNotUnique)
}
return nil
}
func (v Validator) doesAdminExistByEmail(value interface{}) error {
email, ok := value.(*string)
if !ok {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
adminExisted, err := v.repo.AdminExistByEmail(context.Background(), *email)
if err != nil {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
if adminExisted {
return fmt.Errorf(errmsg.ErrorMsgPhoneNumberIsNotUnique)
}
return nil
}
func (v Validator) IsRoleValid(value interface{}) error {
role, ok := value.(*entity.AdminRole)
if !ok {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
if isValid := role.IsValid(); isValid != true {
return fmt.Errorf(errmsg.ErrorMsgInvalidInput)
}
return nil
}
func (v Validator) IsGenderValid(value interface{}) error {
gender, ok := value.(*entity.Gender)
if gender == nil {
return nil
}
if !ok {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
if isValid := gender.IsValid(); isValid != true {
return fmt.Errorf(errmsg.ErrorMsgInvalidInput)
}
return nil
}
func (v Validator) IsStatusValid(value interface{}) error {
status, ok := value.(*entity.AdminStatus)
if !ok {
return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong)
}
if isValid := status.IsValid(); isValid != true {
return fmt.Errorf(errmsg.ErrorMsgInvalidInput)
}
return nil
}