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"
)
@ -13,10 +14,11 @@ type HTTPServer struct {
}
type Config struct {
HTTPServer HTTPServer `koanf:"http_server"`
Mysql mysql.Config `koanf:"mysql"`
Auth authservice.Config `koanf:"auth"`
Redis redis.Config `koanf:"redis"`
KavenegarSmsProvider smsprovider.Config `koanf:"kavenegar_sms_provider"`
BenefactorSvc benefactorservice.Config `koanf:"benefactor_service"`
HTTPServer HTTPServer `koanf:"http_server"`
Mysql mysql.Config `koanf:"mysql"`
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
PhoneNumber string
Role AdminRole
Address string
Description string
Email string
City string
Gender Gender
Status AdminStatus
Birthday time.Time
StatusChangedAt time.Time
ID uint
FirstName string
LastName string
password string
PhoneNumber string
Role AdminRole
Description string
Email string
Gender Gender
Status AdminStatus
}
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

@ -1,15 +1,17 @@
package errmsg
const (
ErrorMsgNotFound = "record not found"
ErrorMsgSomethingWentWrong = "something went wrong"
ErrorMsgInvalidInput = "invalid input"
ErrorMsgInvalidStatus = "invalid status"
ErrorMsgPhoneNumberIsNotUnique = "phone number 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"
ErrorMsgNotFound = "record not found"
ErrorMsgSomethingWentWrong = "something went wrong"
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
}