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_new_user: ebhomeverify
otp_template_registered_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" "git.gocasts.ir/ebhomengo/niki/adapter/redis"
smsprovider "git.gocasts.ir/ebhomengo/niki/adapter/sms_provider/kavenegar" smsprovider "git.gocasts.ir/ebhomengo/niki/adapter/sms_provider/kavenegar"
"git.gocasts.ir/ebhomengo/niki/repository/mysql" "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" benefactorservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/benefactor"
) )
@ -15,7 +16,8 @@ type HTTPServer struct {
type Config struct { type Config struct {
HTTPServer HTTPServer `koanf:"http_server"` HTTPServer HTTPServer `koanf:"http_server"`
Mysql mysql.Config `koanf:"mysql"` 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"` Redis redis.Config `koanf:"redis"`
KavenegarSmsProvider smsprovider.Config `koanf:"kavenegar_sms_provider"` KavenegarSmsProvider smsprovider.Config `koanf:"kavenegar_sms_provider"`
BenefactorSvc benefactorservice.Config `koanf:"benefactor_service"` BenefactorSvc benefactorservice.Config `koanf:"benefactor_service"`

View File

@ -12,4 +12,5 @@ const (
AccessTokenExpireDuration = time.Hour * 24 AccessTokenExpireDuration = time.Hour * 24
RefreshTokenExpireDuration = time.Hour * 24 * 7 RefreshTokenExpireDuration = time.Hour * 24 * 7
AuthMiddlewareContextKey = "claims" 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 ( import (
"fmt" "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" config "git.gocasts.ir/ebhomengo/niki/config"
benefactorbasehandler "git.gocasts.ir/ebhomengo/niki/delivery/http_server/benefactor/base" benefactorbasehandler "git.gocasts.ir/ebhomengo/niki/delivery/http_server/benefactor/base"
@ -23,6 +27,7 @@ type Server struct {
benefactorHandler benefactorhandler.Handler benefactorHandler benefactorhandler.Handler
benefactorKindBoxReqHandler benefactorkindboxreqhandler.Handler benefactorKindBoxReqHandler benefactorkindboxreqhandler.Handler
benefactorBaseHandler benefactorbasehandler.Handler benefactorBaseHandler benefactorbasehandler.Handler
adminHandler adminhandler.Handler
} }
func New( func New(
@ -33,6 +38,9 @@ func New(
benefactorKindBoxReqSvc benefactorkindboxreqservice.Service, benefactorKindBoxReqSvc benefactorkindboxreqservice.Service,
benefactorKindBoxReqVld benefactorkindboxreqvalidator.Validator, benefactorKindBoxReqVld benefactorkindboxreqvalidator.Validator,
benefactorAddressSvc benefactoraddressservice.Service, benefactorAddressSvc benefactoraddressservice.Service,
adminSvc adminservice.Service,
adminVld adminvalidator.Validator,
adminAuthSvc adminauthservice.Service,
) Server { ) Server {
return Server{ return Server{
Router: echo.New(), Router: echo.New(),
@ -40,6 +48,7 @@ func New(
benefactorHandler: benefactorhandler.New(cfg.Auth, authSvc, benefactorSvc, benefactorVld, benefactorAddressSvc), benefactorHandler: benefactorhandler.New(cfg.Auth, authSvc, benefactorSvc, benefactorVld, benefactorAddressSvc),
benefactorKindBoxReqHandler: benefactorkindboxreqhandler.New(cfg.Auth, authSvc, benefactorKindBoxReqSvc, benefactorKindBoxReqVld), benefactorKindBoxReqHandler: benefactorkindboxreqhandler.New(cfg.Auth, authSvc, benefactorKindBoxReqSvc, benefactorKindBoxReqVld),
benefactorBaseHandler: benefactorbasehandler.New(benefactorAddressSvc), 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.benefactorHandler.SetRoutes(s.Router)
s.benefactorKindBoxReqHandler.SetRoutes(s.Router) s.benefactorKindBoxReqHandler.SetRoutes(s.Router)
s.benefactorBaseHandler.SetRoutes(s.Router) s.benefactorBaseHandler.SetRoutes(s.Router)
s.adminHandler.SetRoutes(s.Router)
// Start server // Start server
address := fmt.Sprintf(":%d", s.config.HTTPServer.Port) address := fmt.Sprintf(":%d", s.config.HTTPServer.Port)

View File

@ -1,19 +1,23 @@
package entity package entity
import "time"
type Admin struct { type Admin struct {
ID uint ID uint
FirstName string FirstName string
LastName string LastName string
password string
PhoneNumber string PhoneNumber string
Role AdminRole Role AdminRole
Address string
Description string Description string
Email string Email string
City string
Gender Gender Gender Gender
Status AdminStatus 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] 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. // AllAdminRole returns a slice containing all string values of AdminRole.
func AllAdminRole() []string { func AllAdminRole() []string {
roleStrings := make([]string, len(AdminRoleStrings)) roleStrings := make([]string, len(AdminRoleStrings))

View File

@ -16,6 +16,10 @@ func (s AdminStatus) String() string {
return AdminStatusStrings[s] 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. // AllAdminStatus returns a slice containing all string values of AdminStatus.
func AllAdminStatus() []string { func AllAdminStatus() []string {
statusStrings := make([]string, len(AdminStatusStrings)) statusStrings := make([]string, len(AdminStatusStrings))

View File

@ -26,6 +26,10 @@ func AllGender() []string {
return statusStrings return statusStrings
} }
func (s Gender) IsValid() bool {
return s > 0 && int(s) <= len(GenderStrings)
}
// MapToGender converts a string to the corresponding Gender value. // MapToGender converts a string to the corresponding Gender value.
func MapToGender(statusStr string) Gender { func MapToGender(statusStr string) Gender {
for status, str := range GenderStrings { 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/migrator"
"git.gocasts.ir/ebhomengo/niki/repository/mysql" "git.gocasts.ir/ebhomengo/niki/repository/mysql"
mysqladdress "git.gocasts.ir/ebhomengo/niki/repository/mysql/address" 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" mysqlbenefactor "git.gocasts.ir/ebhomengo/niki/repository/mysql/benefactor"
mysqlkindboxreq "git.gocasts.ir/ebhomengo/niki/repository/mysql/kind_box_req" mysqlkindboxreq "git.gocasts.ir/ebhomengo/niki/repository/mysql/kind_box_req"
redisotp "git.gocasts.ir/ebhomengo/niki/repository/redis/redis_otp" 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" authservice "git.gocasts.ir/ebhomengo/niki/service/auth/benefactor"
benefactoraddressservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/address" benefactoraddressservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/address"
benefactorservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/benefactor" benefactorservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/benefactor"
benefactorkindboxreqservice "git.gocasts.ir/ebhomengo/niki/service/benefactor/kind_box_req" 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" benefactorvalidator "git.gocasts.ir/ebhomengo/niki/validator/benefactor/benefactor"
benefactorkindboxreqvalidator "git.gocasts.ir/ebhomengo/niki/validator/benefactor/kind_box_req" benefactorkindboxreqvalidator "git.gocasts.ir/ebhomengo/niki/validator/benefactor/kind_box_req"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
@ -27,8 +31,10 @@ func main() {
mgr := migrator.New(cfg.Mysql) mgr := migrator.New(cfg.Mysql)
mgr.Up() mgr.Up()
authSvc, benefactorSvc, benefactorVld, benefactorKindBoxReqSvc, benefactorKindBoxReqVld, benefactorAddressSvc := setupServices(cfg) authSvc, benefactorSvc, benefactorVld, benefactorKindBoxReqSvc, benefactorKindBoxReqVld, benefactorAddressSvc,
server := httpserver.New(cfg, benefactorSvc, benefactorVld, authSvc, benefactorKindBoxReqSvc, benefactorKindBoxReqVld, benefactorAddressSvc) adminSvc, adminVld, adminAuthSvc := setupServices(cfg)
server := httpserver.New(cfg, benefactorSvc, benefactorVld, authSvc, benefactorKindBoxReqSvc, benefactorKindBoxReqVld,
benefactorAddressSvc, adminSvc, adminVld, adminAuthSvc)
server.Serve() server.Serve()
} }
@ -36,7 +42,7 @@ func main() {
func setupServices(cfg config.Config) ( func setupServices(cfg config.Config) (
authSvc authservice.Service, benefactorSvc benefactorservice.Service, benefactorVld benefactorvalidator.Validator, authSvc authservice.Service, benefactorSvc benefactorservice.Service, benefactorVld benefactorvalidator.Validator,
benefactorKindBoxReqSvc benefactorkindboxreqservice.Service, benefactorKindBoxReqVld benefactorkindboxreqvalidator.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) authSvc = authservice.New(cfg.Auth)
@ -58,5 +64,10 @@ func setupServices(cfg config.Config) (
benefactorKindBoxReqSvc = benefactorkindboxreqservice.New(benefactorKindBoxReqMysql) benefactorKindBoxReqSvc = benefactorkindboxreqservice.New(benefactorKindBoxReqMysql)
benefactorKindBoxReqVld = benefactorkindboxreqvalidator.New(benefactorKindBoxReqMysql, benefactorSvc, benefactorAddressSvc) 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 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" ErrorMsgInvalidInput = "invalid input"
ErrorMsgInvalidStatus = "invalid status" ErrorMsgInvalidStatus = "invalid status"
ErrorMsgPhoneNumberIsNotUnique = "phone number is not unique" ErrorMsgPhoneNumberIsNotUnique = "phone number is not unique"
ErrorMsgEmailIsNotUnique = "email is not unique"
ErrorMsgPhoneNumberIsNotValid = "phone number is not valid" ErrorMsgPhoneNumberIsNotValid = "phone number is not valid"
ErrorMsgUserNotAllowed = "user not allowed" ErrorMsgUserNotAllowed = "user not allowed"
ErrorMsgUserNotFound = "benefactor not found" ErrorMsgUserNotFound = "benefactor not found"
ErrorMsgOtpCodeExist = "please wait a little bit" ErrorMsgOtpCodeExist = "please wait a little bit"
ErrorMsgOtpCodeIsNotValid = "verification code is not valid" ErrorMsgOtpCodeIsNotValid = "verification code is not valid"
ErrorMsgCantScanQueryResult = "can't scan query result" 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 ( import (
"git.gocasts.ir/ebhomengo/niki/entity"
"github.com/golang-jwt/jwt/v4"
"strings"
"time" "time"
) )
@ -21,3 +24,54 @@ func New(cfg Config) Service {
config: cfg, 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) _, dErr := s.redisOtp.DeleteCodeByPhoneNumber(ctx, req.PhoneNumber)
if dErr != nil { 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) 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, Role: entity.UserBenefactorRole,
}) })
if err != nil { 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 benefactor = newBenefactor
} }
accessToken, err := s.auth.CreateAccessToken(benefactor) accessToken, aErr := s.auth.CreateAccessToken(benefactor)
if err != nil { if aErr != nil {
return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected) return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(aErr).WithKind(richerror.KindUnexpected)
} }
refreshToken, err := s.auth.CreateRefreshToken(benefactor) refreshToken, rErr := s.auth.CreateRefreshToken(benefactor)
if err != nil { if rErr != nil {
return benefactoreparam.LoginOrRegisterResponse{}, richerror.New(op).WithErr(rErr).WithKind(richerror.KindUnexpected) 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
}