diff --git a/database/new_db.go b/database/new_db.go index fe6e821..7e93bbc 100644 --- a/database/new_db.go +++ b/database/new_db.go @@ -5,6 +5,8 @@ import ( "os" staffmodel "github.com/ardeman/project-legalgo-go/database/staff" + subsmodel "github.com/ardeman/project-legalgo-go/database/subscribe" + usermodel "github.com/ardeman/project-legalgo-go/database/user" "gorm.io/driver/postgres" "gorm.io/gorm" ) @@ -39,5 +41,10 @@ func NewDB() (*DB, error) { func (db *DB) Migrate() error { // Auto Migrate the User model - return db.AutoMigrate(&staffmodel.Staff{}) + return db.AutoMigrate( + &staffmodel.Staff{}, + &subsmodel.SubscribePlan{}, + &subsmodel.Subscribe{}, + &usermodel.User{}, + ) } diff --git a/database/staff/model.go b/database/staff/model.go index e43ce93..fee793f 100644 --- a/database/staff/model.go +++ b/database/staff/model.go @@ -1,11 +1,15 @@ package staffmodel import ( + "time" + "github.com/google/uuid" ) type Staff struct { - ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"` - Email string `gorm:"unique,not null" json:"email"` - Password string `gorm:"not null" json:"password"` + ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"` + Email string `gorm:"unique,not null" json:"email"` + Password string `gorm:"not null" json:"password"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` + UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` } diff --git a/database/subscribe/model.go b/database/subscribe/model.go new file mode 100644 index 0000000..0d75524 --- /dev/null +++ b/database/subscribe/model.go @@ -0,0 +1,27 @@ +package subsmodel + +import ( + "time" + + "github.com/google/uuid" +) + +type Subscribe struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"` + SubscribePlanID uuid.UUID `gorm:"type:uuid;not null" json:"subscribe_plan_id"` + StartDate time.Time `gorm:"default:CURRENT_TIMESTAMP"` + EndDate time.Time `gorm:"default:null"` + Status string `gorm:"default:'inactive'"` + AutoRenew bool `gorm:"default:true"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"` + UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"` + + SubscribePlan SubscribePlan `gorm:"foreignKey:SubscribePlanID;constraint:OnDelete:CASCADE"` +} + +type SubscribePlan struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"` + Code string `gorm:"not null" json:"code"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"` + UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP"` +} diff --git a/database/user/model.go b/database/user/model.go new file mode 100644 index 0000000..70294ad --- /dev/null +++ b/database/user/model.go @@ -0,0 +1,19 @@ +package usermodel + +import ( + "time" + + subsmodel "github.com/ardeman/project-legalgo-go/database/subscribe" + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"` + SubscribeID string `gorm:"not null" json:"subscribe_id"` + Email string `gorm:"unique,not null" json:"email"` + Password string `gorm:"not null" json:"password"` + Phone string `gorm:"default:null" json:"phone"` + Subscribe subsmodel.Subscribe `gorm:"foreignKey:SubscribeID;constraint:OnDelete:CASCADE"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` + UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` +} diff --git a/internal/accessor/module.go b/internal/accessor/module.go index 7bc49e5..3be5a3e 100644 --- a/internal/accessor/module.go +++ b/internal/accessor/module.go @@ -1,10 +1,18 @@ package repository import ( + redisaccessor "github.com/ardeman/project-legalgo-go/internal/accessor/redis" staffrepository "github.com/ardeman/project-legalgo-go/internal/accessor/staff" + subscriberepository "github.com/ardeman/project-legalgo-go/internal/accessor/subscribe" + subscribeplanrepository "github.com/ardeman/project-legalgo-go/internal/accessor/subscribeplan" + userrepository "github.com/ardeman/project-legalgo-go/internal/accessor/user_repository" "go.uber.org/fx" ) var Module = fx.Module("repository", fx.Provide( staffrepository.New, + userrepository.New, + redisaccessor.New, + subscribeplanrepository.New, + subscriberepository.New, )) diff --git a/internal/accessor/redis/impl.go b/internal/accessor/redis/impl.go new file mode 100644 index 0000000..532f00c --- /dev/null +++ b/internal/accessor/redis/impl.go @@ -0,0 +1,32 @@ +package redisaccessor + +import ( + "fmt" + "os" + + "github.com/redis/go-redis/v9" +) + +var redisClient *redis.Client + +func Get() *redis.Client { + return redisClient +} + +func New() *redis.Client { + var ( + username = os.Getenv("REDIS_USERNAME") + addr = fmt.Sprintf("%s:%s", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT")) + password = os.Getenv("REDIS_PASSWORD") + db = 2 // TODO: change later + ) + + redisClient = redis.NewClient(&redis.Options{ + Username: username, + Addr: addr, + Password: password, + DB: db, + }) + + return redisClient +} diff --git a/internal/accessor/staff/create_staff.go b/internal/accessor/staff/create_staff.go new file mode 100644 index 0000000..2b416d6 --- /dev/null +++ b/internal/accessor/staff/create_staff.go @@ -0,0 +1,11 @@ +package staffrepository + +import authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" + +func (ur *StaffRepository) Create(spec *authdomain.Staff) (*authdomain.Staff, error) { + if err := ur.DB.Create(&spec).Error; err != nil { + return nil, err + } + + return spec, nil +} diff --git a/internal/accessor/staff/get_staff.go b/internal/accessor/staff/get_staff.go index 123f90f..94df39f 100644 --- a/internal/accessor/staff/get_staff.go +++ b/internal/accessor/staff/get_staff.go @@ -3,12 +3,12 @@ package staffrepository import ( "errors" - staffmodel "github.com/ardeman/project-legalgo-go/database/staff" + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" "gorm.io/gorm" ) -func (sr *StaffRepository) GetStaff(email string) (*staffmodel.Staff, error) { - var staff staffmodel.Staff +func (sr *StaffRepository) GetStaffByEmail(email string) (*authdomain.LoginRepoResponse, error) { + var staff authdomain.LoginRepoResponse if email == "" { return nil, errors.New("email is empty") diff --git a/internal/accessor/staff/impl.go b/internal/accessor/staff/impl.go index 3e91285..bf59de2 100644 --- a/internal/accessor/staff/impl.go +++ b/internal/accessor/staff/impl.go @@ -2,17 +2,18 @@ package staffrepository import ( "github.com/ardeman/project-legalgo-go/database" - staffmodel "github.com/ardeman/project-legalgo-go/database/staff" + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" ) type StaffRepository struct { DB *database.DB } -type StaffInterface interface { - GetStaff(string) (*staffmodel.Staff, error) +type StaffIntf interface { + GetStaffByEmail(string) (*authdomain.LoginRepoResponse, error) + Create(*authdomain.Staff) (*authdomain.Staff, error) } -func New(db *database.DB) StaffInterface { +func New(db *database.DB) StaffIntf { return &StaffRepository{db} } diff --git a/internal/accessor/subscribe/create.go b/internal/accessor/subscribe/create.go new file mode 100644 index 0000000..26db13a --- /dev/null +++ b/internal/accessor/subscribe/create.go @@ -0,0 +1,19 @@ +package subscriberepository + +import ( + subscribedomain "github.com/ardeman/project-legalgo-go/internal/domain/subscribe" + "github.com/google/uuid" +) + +func (s *SubsAccs) Create(subsPlanId string) (string, error) { + spec := &subscribedomain.Subscribe{ + ID: uuid.New(), + SubscribePlanID: subsPlanId, + } + + if err := s.DB.Create(&spec).Error; err != nil { + return "", err + } + + return spec.ID.String(), nil +} diff --git a/internal/accessor/subscribe/impl.go b/internal/accessor/subscribe/impl.go new file mode 100644 index 0000000..e0bc4c5 --- /dev/null +++ b/internal/accessor/subscribe/impl.go @@ -0,0 +1,15 @@ +package subscriberepository + +import "github.com/ardeman/project-legalgo-go/database" + +type SubsAccs struct { + DB *database.DB +} + +type SubsIntf interface { + Create(string) (string, error) +} + +func New(db *database.DB) SubsIntf { + return &SubsAccs{db} +} diff --git a/internal/accessor/subscribeplan/create.go b/internal/accessor/subscribeplan/create.go new file mode 100644 index 0000000..e9f936e --- /dev/null +++ b/internal/accessor/subscribeplan/create.go @@ -0,0 +1,19 @@ +package subscribeplanrepository + +import ( + subscribeplandomain "github.com/ardeman/project-legalgo-go/internal/domain/subscribe_plan" + "github.com/google/uuid" +) + +func (s *SubsPlan) Create(code string) error { + spec := &subscribeplandomain.SubscribePlan{ + ID: uuid.New(), + Code: code, + } + + if err := s.DB.Create(&spec).Error; err != nil { + return err + } + + return nil +} diff --git a/internal/accessor/subscribeplan/impl.go b/internal/accessor/subscribeplan/impl.go new file mode 100644 index 0000000..caca8d7 --- /dev/null +++ b/internal/accessor/subscribeplan/impl.go @@ -0,0 +1,17 @@ +package subscribeplanrepository + +import "github.com/ardeman/project-legalgo-go/database" + +type SubsPlan struct { + DB *database.DB +} + +type SubsPlanIntf interface { + Create(string) error +} + +func New( + db *database.DB, +) SubsPlanIntf { + return &SubsPlan{db} +} diff --git a/internal/accessor/user_repository/create_user.go b/internal/accessor/user_repository/create_user.go new file mode 100644 index 0000000..d472d27 --- /dev/null +++ b/internal/accessor/user_repository/create_user.go @@ -0,0 +1,13 @@ +package userrepository + +import ( + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" +) + +func (ur *UserRepository) CreateUser(spec *authdomain.User) (*authdomain.User, error) { + if err := ur.DB.Create(&spec).Error; err != nil { + return nil, err + } + + return spec, nil +} diff --git a/internal/accessor/user_repository/get_user.go b/internal/accessor/user_repository/get_user.go new file mode 100644 index 0000000..31c30e8 --- /dev/null +++ b/internal/accessor/user_repository/get_user.go @@ -0,0 +1,25 @@ +package userrepository + +import ( + "errors" + + usermodel "github.com/ardeman/project-legalgo-go/database/user" + "gorm.io/gorm" +) + +func (ur *UserRepository) GetUserByEmail(email string) (*usermodel.User, error) { + var user usermodel.User + + if email == "" { + return nil, errors.New("email is empty") + } + + if err := ur.DB.Where("email = ?", email).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("user not found") + } + return nil, err + } + + return &user, nil +} diff --git a/internal/accessor/user_repository/impl.go b/internal/accessor/user_repository/impl.go new file mode 100644 index 0000000..2d44e92 --- /dev/null +++ b/internal/accessor/user_repository/impl.go @@ -0,0 +1,22 @@ +package userrepository + +import ( + "github.com/ardeman/project-legalgo-go/database" + usermodel "github.com/ardeman/project-legalgo-go/database/user" + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" +) + +type UserRepository struct { + DB *database.DB +} + +type UserIntf interface { + GetUserByEmail(string) (*usermodel.User, error) + CreateUser(*authdomain.User) (*authdomain.User, error) +} + +func New( + db *database.DB, +) UserIntf { + return &UserRepository{db} +} diff --git a/internal/api/http/auth/login.go b/internal/api/http/auth/login.go index 727461b..77ebc07 100644 --- a/internal/api/http/auth/login.go +++ b/internal/api/http/auth/login.go @@ -3,8 +3,8 @@ package authhttp import ( "net/http" - domain "github.com/ardeman/project-legalgo-go/internal/domain/auth" - serviceauth "github.com/ardeman/project-legalgo-go/internal/services/auth" + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" + authsvc "github.com/ardeman/project-legalgo-go/internal/services/auth" "github.com/ardeman/project-legalgo-go/internal/utilities/response" "github.com/ardeman/project-legalgo-go/internal/utilities/utils" "github.com/go-chi/chi/v5" @@ -13,15 +13,15 @@ import ( func LoginStaff( router chi.Router, - authSvc serviceauth.LoginStaffIntf, + authSvc authsvc.AuthIntf, validate *validator.Validate, ) { router.Post("/staff/login", func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - var request domain.StaffLoginReq + var spec authdomain.LoginReq - if err := utils.UnmarshalBody(r, &request); err != nil { + if err := utils.UnmarshalBody(r, &spec); err != nil { response.ResponseWithErrorCode( ctx, w, @@ -33,7 +33,7 @@ func LoginStaff( return } - if err := validate.Struct(request); err != nil { + if err := validate.Struct(spec); err != nil { response.ResponseWithErrorCode( ctx, w, @@ -45,7 +45,7 @@ func LoginStaff( return } - token, err := authSvc.LoginAsStaff(request.Email) + token, err := authSvc.LoginAsStaff(spec) if err != nil { response.ResponseWithErrorCode( ctx, @@ -58,7 +58,62 @@ func LoginStaff( return } - responsePayload := &domain.StaffLoginResponse{ + responsePayload := &authdomain.LoginResponse{ + Token: token, + } + + response.RespondJsonSuccess(ctx, w, responsePayload) + }) +} + +func LoginUser( + router chi.Router, + authSvc authsvc.AuthIntf, + validate *validator.Validate, +) { + router.Post("/user/login", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var spec authdomain.LoginReq + + if err := utils.UnmarshalBody(r, &spec); err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + "failed to unmarshal request", + ) + return + } + + if err := validate.Struct(spec); err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + err.(validator.ValidationErrors).Error(), + ) + return + } + + token, err := authSvc.LoginAsUser(spec) + if err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + err.Error(), + ) + return + } + + responsePayload := &authdomain.LoginResponse{ Token: token, } diff --git a/internal/api/http/auth/module.go b/internal/api/http/auth/module.go index faba3f8..ebddcd6 100644 --- a/internal/api/http/auth/module.go +++ b/internal/api/http/auth/module.go @@ -5,5 +5,8 @@ import "go.uber.org/fx" var Module = fx.Module("auth-api", fx.Invoke( LoginStaff, + LoginUser, + RegisterUser, + RegisterStaff, ), ) diff --git a/internal/api/http/auth/register.go b/internal/api/http/auth/register.go new file mode 100644 index 0000000..d60d216 --- /dev/null +++ b/internal/api/http/auth/register.go @@ -0,0 +1,114 @@ +package authhttp + +import ( + "net/http" + + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" + authsvc "github.com/ardeman/project-legalgo-go/internal/services/auth" + "github.com/ardeman/project-legalgo-go/internal/utilities/response" + "github.com/ardeman/project-legalgo-go/internal/utilities/utils" + "github.com/go-chi/chi/v5" + "github.com/go-playground/validator/v10" +) + +func RegisterUser( + router chi.Router, + validate *validator.Validate, + authSvc authsvc.AuthIntf, +) { + router.Post("/user/register", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var spec authdomain.RegisterUserReq + + if err := utils.UnmarshalBody(r, &spec); err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + "failed to unmarshal request", + ) + return + } + + if err := validate.Struct(spec); err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + err.(validator.ValidationErrors).Error(), + ) + return + } + + token, err := authSvc.RegisterUser(spec) + if err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + err.Error(), + ) + return + } + + response.RespondJsonSuccess(ctx, w, token) + }) +} + +func RegisterStaff( + router chi.Router, + validate *validator.Validate, + authSvc authsvc.AuthIntf, +) { + router.Post("/staff/register", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var spec authdomain.RegisterStaffReq + + if err := utils.UnmarshalBody(r, &spec); err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + "failed to unmarshal request", + ) + return + } + + if err := validate.Struct(spec); err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + err.(validator.ValidationErrors).Error(), + ) + return + } + + token, err := authSvc.RegisterStaff(spec) + if err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + err.Error(), + ) + return + } + + response.RespondJsonSuccess(ctx, w, token) + }) +} diff --git a/internal/api/http/middleware/auth/authorize.go b/internal/api/http/middleware/auth/authorize.go new file mode 100644 index 0000000..d612a62 --- /dev/null +++ b/internal/api/http/middleware/auth/authorize.go @@ -0,0 +1,138 @@ +package authmiddleware + +// import ( +// "context" +// "fmt" +// "net/http" +// "strings" + +// redisaccessor "github.com/ardeman/project-legalgo-go/internal/accessor/redis" +// contextkeyenum "github.com/ardeman/project-legalgo-go/internal/enums/context_key" +// jwtclaimenum "github.com/ardeman/project-legalgo-go/internal/enums/jwt" +// resourceenum "github.com/ardeman/project-legalgo-go/internal/enums/resource" +// "github.com/ardeman/project-legalgo-go/internal/services/auth" +// "github.com/golang-jwt/jwt/v5" +// ) + +// const SessionHeader = "Authorization" + +// func Authorization() func(next http.Handler) http.Handler { +// return func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// ctx := r.Context() + +// tokenString, err := GetToken(r) +// if err != nil { +// RespondWithError(w, r, err, "Invalid auth header") +// return +// } + +// token, err := ValidateToken(ctx, tokenString) +// if err != nil { +// RespondWithError(w, r, err, err.Error()) +// return +// } + +// if isAuthorized, ctx := VerifyClaims(ctx, token, nil); !isAuthorized { +// RespondWithError(w, r, errorcode.ErrCodeUnauthorized, errorcode.ErrCodeUnauthorized.Message) +// return +// } else { +// next.ServeHTTP(w, r.WithContext(ctx)) +// return +// } +// }) +// } +// } + +// func GetToken(r *http.Request) (string, error) { +// tokenString := GetTokenFromHeader(r) +// if tokenString == "" { +// tokenString = getTokenFromQuery(r) +// } + +// if tokenString == "" { +// return "", fmt.Errorf("token not found") +// } + +// return tokenString, nil +// } + +// func GetTokenFromHeader(r *http.Request) string { +// session := r.Header.Get(SessionHeader) +// arr := strings.Split(session, " ") + +// if len(arr) != 2 || strings.ToUpper(arr[0]) != "BEARER" { +// return "" +// } + +// return arr[1] +// } + +// func getTokenFromQuery(r *http.Request) string { +// token := r.URL.Query().Get("token") +// return token +// } + +// func VerifyClaims(ctx context.Context, token *jwt.Token, +// requiredResources []resourceenum.Resource) (bool, context.Context) { +// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { +// rawResources := []interface{}{} +// if claimValue, exist := claims[string(jwtclaimenum.RESOURCES)]; exist { +// rawResources = claimValue.([]interface{}) +// } + +// resources := []resourceenum.Resource{} +// resourceMap := map[string]bool{} + +// for _, v := range rawResources { +// value := v.(string) +// resources = append(resources, resourceenum.Resource(value)) +// resourceMap[value] = true +// } + +// ctx = context.WithValue(ctx, contextkeyenum.Authorization, UserAuthorization{ +// Type: claims[string(jwtclaimenum.TYPE)].(string), +// UserId: claims[string(jwtclaimenum.AUDIENCE)].(string), +// Username: claims[string(jwtclaimenum.USERNAME)].(string), +// Resources: resources, +// }) + +// isResourceFulfilled := false + +// for _, v := range requiredResources { +// if _, ok := resourceMap[string(v)]; ok { +// isResourceFulfilled = true +// ctx = context.WithValue(ctx, contextkeyenum.Resource, v) + +// break +// } +// } + +// if isResourceFulfilled || len(requiredResources) == 0 { +// return true, ctx +// } +// } + +// return false, nil +// } + +// func ValidateToken(ctx context.Context, tokenString string) (*jwt.Token, error) { +// redisClient := redisaccessor.Get() +// redisToken, err := redisClient.Exists(ctx, fmt.Sprintf("%s:%s", auth.BLACKLISTED_TOKEN_KEY, tokenString)).Result() + +// if err != nil || redisToken > 0 { +// return nil, fmt.Errorf("session already expired") +// } + +// token, err := jwt.Parse(tokenString, authsvc.VerifyToken(conf.JWTAccessToken)) +// if err != nil { +// if ve, ok := err.(*jwt.ValidationError); ok { +// if ve.Errors&jwt.ValidationErrorExpired != 0 { +// err = errorcode.ErrCodeExpiredToken +// } +// } +// return nil, err +// } + +// return token, nil +// } diff --git a/internal/api/http/router.go b/internal/api/http/router.go index a38b667..79e4617 100644 --- a/internal/api/http/router.go +++ b/internal/api/http/router.go @@ -2,6 +2,7 @@ package internalhttp import ( authhttp "github.com/ardeman/project-legalgo-go/internal/api/http/auth" + subscribeplanhttp "github.com/ardeman/project-legalgo-go/internal/api/http/subscribe_plan" "github.com/go-chi/chi/v5" "github.com/go-chi/cors" "github.com/go-playground/validator/v10" @@ -16,6 +17,7 @@ var Module = fx.Module("router", validator.New, ), authhttp.Module, + subscribeplanhttp.Module, ) func initRouter() chi.Router { diff --git a/internal/api/http/subscribe_plan/create.go b/internal/api/http/subscribe_plan/create.go new file mode 100644 index 0000000..ff68e29 --- /dev/null +++ b/internal/api/http/subscribe_plan/create.go @@ -0,0 +1,66 @@ +package subscribeplanhttp + +import ( + "net/http" + + subscribeplandomain "github.com/ardeman/project-legalgo-go/internal/domain/subscribe_plan" + subscribeplansvc "github.com/ardeman/project-legalgo-go/internal/services/subscribe_plan" + "github.com/ardeman/project-legalgo-go/internal/utilities/response" + "github.com/ardeman/project-legalgo-go/internal/utilities/utils" + "github.com/go-chi/chi/v5" + "github.com/go-playground/validator/v10" +) + +func CreateSubscribePlan( + router chi.Router, + validate *validator.Validate, + subsSvc subscribeplansvc.SubsPlanIntf, +) { + router.Post("/subscribe-plan/create", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var spec subscribeplandomain.SubscribePlanReq + + if err := utils.UnmarshalBody(r, &spec); err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + "failed to unmarshal request", + ) + return + } + + if err := validate.Struct(spec); err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrBadRequest.Code, + response.ErrBadRequest.HttpCode, + err.(validator.ValidationErrors).Error(), + ) + return + } + + if err := subsSvc.CreatePlan(spec.Code); err != nil { + response.ResponseWithErrorCode( + ctx, + w, + err, + response.ErrCreateEntity.Code, + response.ErrCreateEntity.HttpCode, + err.Error(), + ) + return + } + + response.RespondJsonSuccess(ctx, w, struct { + Message string + }{ + Message: "success", + }) + }) +} diff --git a/internal/api/http/subscribe_plan/module.go b/internal/api/http/subscribe_plan/module.go new file mode 100644 index 0000000..853a200 --- /dev/null +++ b/internal/api/http/subscribe_plan/module.go @@ -0,0 +1,9 @@ +package subscribeplanhttp + +import "go.uber.org/fx" + +var Module = fx.Module("subscribe-plan", + fx.Invoke( + CreateSubscribePlan, + ), +) diff --git a/internal/config/chi_router.go b/internal/config/chi_router.go index d1a7a6e..d197f88 100644 --- a/internal/config/chi_router.go +++ b/internal/config/chi_router.go @@ -16,7 +16,7 @@ import ( func Router(apiRouter chi.Router) { mainRouter := chi.NewRouter() - mainRouter.Mount("/", apiRouter) + mainRouter.Mount("/api", apiRouter) mainCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() diff --git a/internal/domain/auth/login.go b/internal/domain/auth/login.go new file mode 100644 index 0000000..f216642 --- /dev/null +++ b/internal/domain/auth/login.go @@ -0,0 +1,18 @@ +package authdomain + +import "github.com/google/uuid" + +type LoginReq struct { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` +} + +type LoginResponse struct { + Token string `json:"token"` +} + +type LoginRepoResponse struct { + ID uuid.UUID `json:"id"` + Email string `json:"email"` + Password string `json:"password"` +} diff --git a/internal/domain/auth/register.go b/internal/domain/auth/register.go new file mode 100644 index 0000000..d6c46db --- /dev/null +++ b/internal/domain/auth/register.go @@ -0,0 +1,29 @@ +package authdomain + +import "github.com/google/uuid" + +type RegisterUserReq struct { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Phone string `json:"phone"` + SubscribePlanID string `json:"subscribe_plan_id"` +} + +type User struct { + ID uuid.UUID `json:"id"` + Email string `json:"email"` + Password string `json:"password"` + Phone string `json:"phone"` + SubscribeID string `json:"subscribe_id"` +} + +type RegisterStaffReq struct { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` +} + +type Staff struct { + ID uuid.UUID `json:"id"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` +} diff --git a/internal/domain/auth/staff.go b/internal/domain/auth/staff.go deleted file mode 100644 index 35201a5..0000000 --- a/internal/domain/auth/staff.go +++ /dev/null @@ -1,10 +0,0 @@ -package domain - -type StaffLoginReq struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` -} - -type StaffLoginResponse struct { - Token string `json:"token"` -} diff --git a/internal/domain/caching/spec.go b/internal/domain/caching/spec.go new file mode 100644 index 0000000..13e7359 --- /dev/null +++ b/internal/domain/caching/spec.go @@ -0,0 +1,9 @@ +package cachingdomain + +import "time" + +type CacheSpec struct { + Key string + TTL time.Duration + Data any +} diff --git a/internal/domain/subscribe/spec.go b/internal/domain/subscribe/spec.go new file mode 100644 index 0000000..152e3da --- /dev/null +++ b/internal/domain/subscribe/spec.go @@ -0,0 +1,8 @@ +package subscribedomain + +import "github.com/google/uuid" + +type Subscribe struct { + ID uuid.UUID `json:"id"` + SubscribePlanID string `json:"subscribe_plan_id"` +} diff --git a/internal/domain/subscribe_plan/spec.go b/internal/domain/subscribe_plan/spec.go new file mode 100644 index 0000000..8fa5134 --- /dev/null +++ b/internal/domain/subscribe_plan/spec.go @@ -0,0 +1,12 @@ +package subscribeplandomain + +import "github.com/google/uuid" + +type SubscribePlanReq struct { + Code string `json:"code" validate:"required"` +} + +type SubscribePlan struct { + ID uuid.UUID `json:"id"` + Code string `json:"code"` +} diff --git a/internal/enums/context_key/context_key.go b/internal/enums/context_key/context_key.go new file mode 100644 index 0000000..cd9fc69 --- /dev/null +++ b/internal/enums/context_key/context_key.go @@ -0,0 +1,8 @@ +package contextkeyenum + +type ContextKey string + +const ( + Authorization ContextKey = "AUTHORIZATION_CONTEXT" + Resource ContextKey = "RESOURCE_CONTEXT" +) diff --git a/internal/enums/jwt/jwt_claims.go b/internal/enums/jwt/jwt_claims.go index d58239e..c6a5ab5 100644 --- a/internal/enums/jwt/jwt_claims.go +++ b/internal/enums/jwt/jwt_claims.go @@ -8,4 +8,5 @@ const ( EXPIRED_AT JWTClaim = "exp" SESSION_ID JWTClaim = "sid" ISSUED_AT JWTClaim = "iat" + RESOURCES JWTClaim = "resources" ) diff --git a/internal/enums/resource/resource.go b/internal/enums/resource/resource.go new file mode 100644 index 0000000..635ba77 --- /dev/null +++ b/internal/enums/resource/resource.go @@ -0,0 +1,3 @@ +package resourceenum + +type Resource string diff --git a/internal/services/auth/impl.go b/internal/services/auth/impl.go index b53f9cd..8ea2eed 100644 --- a/internal/services/auth/impl.go +++ b/internal/services/auth/impl.go @@ -1,19 +1,33 @@ -package serviceauth +package authsvc -import staffrepository "github.com/ardeman/project-legalgo-go/internal/accessor/staff" +import ( + staffrepository "github.com/ardeman/project-legalgo-go/internal/accessor/staff" + subscriberepository "github.com/ardeman/project-legalgo-go/internal/accessor/subscribe" + userrepository "github.com/ardeman/project-legalgo-go/internal/accessor/user_repository" + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" +) -type LoginStaffSvc struct { - staffAcs staffrepository.StaffInterface +type AuthSvc struct { + staffRepo staffrepository.StaffIntf + userRepo userrepository.UserIntf + subsRepo subscriberepository.SubsIntf } -type LoginStaffIntf interface { - LoginAsStaff(string) (string, error) +type AuthIntf interface { + LoginAsStaff(authdomain.LoginReq) (string, error) + LoginAsUser(authdomain.LoginReq) (string, error) + RegisterUser(authdomain.RegisterUserReq) (string, error) + RegisterStaff(authdomain.RegisterStaffReq) (string, error) } func New( - staffAcs staffrepository.StaffInterface, -) LoginStaffIntf { - return &LoginStaffSvc{ - staffAcs: staffAcs, + staffRepo staffrepository.StaffIntf, + userRepo userrepository.UserIntf, + subsRepo subscriberepository.SubsIntf, +) AuthIntf { + return &AuthSvc{ + staffRepo: staffRepo, + userRepo: userRepo, + subsRepo: subsRepo, } } diff --git a/internal/services/auth/login_as_staff.go b/internal/services/auth/login_as_staff.go index 554ac66..ea0c7d3 100644 --- a/internal/services/auth/login_as_staff.go +++ b/internal/services/auth/login_as_staff.go @@ -1,18 +1,24 @@ -package serviceauth +package authsvc import ( "errors" + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" "github.com/ardeman/project-legalgo-go/internal/utilities/utils" ) -func (sv *LoginStaffSvc) LoginAsStaff(email string) (string, error) { - staff, err := sv.staffAcs.GetStaff(email) +func (sv *AuthSvc) LoginAsStaff(spec authdomain.LoginReq) (string, error) { + staff, err := sv.staffRepo.GetStaffByEmail(spec.Email) if err != nil { return "", errors.New(err.Error()) } - token, err := utils.GenerateToken2(staff.Email) + matchPassword := ComparePassword(staff.Password, spec.Password) + if !matchPassword { + return "", errors.New("wrong password") + } + + token, err := utils.GenerateToken(staff.Email) if err != nil { return "", errors.New(err.Error()) } diff --git a/internal/services/auth/login_as_user.go b/internal/services/auth/login_as_user.go new file mode 100644 index 0000000..b68d56f --- /dev/null +++ b/internal/services/auth/login_as_user.go @@ -0,0 +1,27 @@ +package authsvc + +import ( + "errors" + + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" + "github.com/ardeman/project-legalgo-go/internal/utilities/utils" +) + +func (sv *AuthSvc) LoginAsUser(spec authdomain.LoginReq) (string, error) { + user, err := sv.userRepo.GetUserByEmail(spec.Email) + if err != nil { + return "", errors.New(err.Error()) + } + + matchPassword := ComparePassword(user.Password, spec.Password) + if !matchPassword { + return "", errors.New("wrong password") + } + + token, err := utils.GenerateToken(user.Email) + if err != nil { + return "", errors.New(err.Error()) + } + + return token, nil +} diff --git a/internal/services/auth/register_staff.go b/internal/services/auth/register_staff.go new file mode 100644 index 0000000..25f1578 --- /dev/null +++ b/internal/services/auth/register_staff.go @@ -0,0 +1,33 @@ +package authsvc + +import ( + "errors" + + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" + "github.com/ardeman/project-legalgo-go/internal/utilities/utils" + "github.com/google/uuid" +) + +func (a *AuthSvc) RegisterStaff(spec authdomain.RegisterStaffReq) (string, error) { + hashedPwd, err := HashPassword(spec.Password) + if err != nil { + return "", err + } + + user := authdomain.Staff{ + ID: uuid.New(), + Email: spec.Email, + Password: hashedPwd, + } + + _, err = a.staffRepo.Create(&user) + if err != nil { + return "", errors.New(err.Error()) + } + + token, err := utils.GenerateToken(spec.Email) + if err != nil { + return "", errors.New(err.Error()) + } + return token, nil +} diff --git a/internal/services/auth/register_user.go b/internal/services/auth/register_user.go new file mode 100644 index 0000000..9a273fa --- /dev/null +++ b/internal/services/auth/register_user.go @@ -0,0 +1,40 @@ +package authsvc + +import ( + "errors" + + authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth" + "github.com/ardeman/project-legalgo-go/internal/utilities/utils" + "github.com/google/uuid" +) + +func (a *AuthSvc) RegisterUser(spec authdomain.RegisterUserReq) (string, error) { + subsId, err := a.subsRepo.Create(spec.SubscribePlanID) + if err != nil { + return "", nil + } + + hashedPwd, err := HashPassword(spec.Password) + if err != nil { + return "", err + } + + user := authdomain.User{ + ID: uuid.New(), + Email: spec.Email, + SubscribeID: subsId, + Password: hashedPwd, + Phone: spec.Phone, + } + + _, err = a.userRepo.CreateUser(&user) + if err != nil { + return "", errors.New(err.Error()) + } + + token, err := utils.GenerateToken(spec.Email) + if err != nil { + return "", errors.New(err.Error()) + } + return token, nil +} diff --git a/internal/services/auth/utils.go b/internal/services/auth/utils.go new file mode 100644 index 0000000..b48c8e6 --- /dev/null +++ b/internal/services/auth/utils.go @@ -0,0 +1,32 @@ +package authsvc + +import ( + "fmt" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +func VerifyToken(signature string) jwt.Keyfunc { + return func(token *jwt.Token) (any, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + return []byte(signature), nil + } +} + +func HashPassword(password string) (string, error) { + // Hashing the password with a cost of 14 (default) + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hash), nil +} + +func ComparePassword(storedPassword, inputPassword string) bool { + err := bcrypt.CompareHashAndPassword([]byte(storedPassword), []byte(inputPassword)) + return err == nil +} diff --git a/internal/services/caching/impl.go b/internal/services/caching/impl.go new file mode 100644 index 0000000..45633d6 --- /dev/null +++ b/internal/services/caching/impl.go @@ -0,0 +1,20 @@ +package cachingsvc + +import ( + "context" + + cachingdomain "github.com/ardeman/project-legalgo-go/internal/domain/caching" + "github.com/redis/go-redis/v9" +) + +type impl struct { + redisClient *redis.Client +} + +type implIntf interface { + Set(context.Context, cachingdomain.CacheSpec) error +} + +func New() implIntf { + return &impl{} +} diff --git a/internal/services/caching/set.go b/internal/services/caching/set.go new file mode 100644 index 0000000..894c1c7 --- /dev/null +++ b/internal/services/caching/set.go @@ -0,0 +1,31 @@ +package cachingsvc + +import ( + "context" + "encoding/json" + + cachingdomain "github.com/ardeman/project-legalgo-go/internal/domain/caching" + "github.com/ardeman/project-legalgo-go/internal/utilities/response" + "github.com/sirupsen/logrus" +) + +func (i *impl) Set(ctx context.Context, spec cachingdomain.CacheSpec) error { + redisClient := i.redisClient + + if redisClient == nil { + logrus.WithContext(ctx).Errorf("redis not found") + + return response.ErrRedisConnNotFound + } + + dataBytes, err := json.Marshal(spec.Data) + if err != nil { + return response.ErrMarshal + } + + if err := redisClient.Set(ctx, spec.Key, string(dataBytes), spec.TTL); err != nil { + return response.ErrRedisSet + } + + return nil +} diff --git a/internal/services/module.go b/internal/services/module.go index 545f4a3..ee2acf7 100644 --- a/internal/services/module.go +++ b/internal/services/module.go @@ -2,11 +2,15 @@ package services import ( serviceauth "github.com/ardeman/project-legalgo-go/internal/services/auth" + subscribesvc "github.com/ardeman/project-legalgo-go/internal/services/subscribe" + subscribeplansvc "github.com/ardeman/project-legalgo-go/internal/services/subscribe_plan" "go.uber.org/fx" ) var Module = fx.Module("services", fx.Provide( serviceauth.New, + subscribeplansvc.New, + subscribesvc.New, ), ) diff --git a/internal/services/subscribe/create.go b/internal/services/subscribe/create.go new file mode 100644 index 0000000..4a0b1e5 --- /dev/null +++ b/internal/services/subscribe/create.go @@ -0,0 +1,9 @@ +package subscribesvc + +func (s *SubsSvc) Create(subsPlanId string) (string, error) { + subsId, err := s.subsRepo.Create(subsPlanId) + if err != nil { + return "", err + } + return subsId, nil +} diff --git a/internal/services/subscribe/impl.go b/internal/services/subscribe/impl.go new file mode 100644 index 0000000..2a1881a --- /dev/null +++ b/internal/services/subscribe/impl.go @@ -0,0 +1,15 @@ +package subscribesvc + +import subscriberepository "github.com/ardeman/project-legalgo-go/internal/accessor/subscribe" + +type SubsSvc struct { + subsRepo subscriberepository.SubsIntf +} + +type SubsIntf interface { + Create(string) (string, error) +} + +func New(subsRepo subscriberepository.SubsIntf) SubsIntf { + return &SubsSvc{subsRepo} +} diff --git a/internal/services/subscribe_plan/create_plan.go b/internal/services/subscribe_plan/create_plan.go new file mode 100644 index 0000000..b54c1b6 --- /dev/null +++ b/internal/services/subscribe_plan/create_plan.go @@ -0,0 +1,5 @@ +package subscribeplansvc + +func (sb *SubsPlanSvc) CreatePlan(code string) error { + return sb.subsAccs.Create(code) +} diff --git a/internal/services/subscribe_plan/impl.go b/internal/services/subscribe_plan/impl.go new file mode 100644 index 0000000..a85d28f --- /dev/null +++ b/internal/services/subscribe_plan/impl.go @@ -0,0 +1,17 @@ +package subscribeplansvc + +import subscribeplanrepository "github.com/ardeman/project-legalgo-go/internal/accessor/subscribeplan" + +type SubsPlanSvc struct { + subsAccs subscribeplanrepository.SubsPlanIntf +} + +type SubsPlanIntf interface { + CreatePlan(string) error +} + +func New( + subsAccs subscribeplanrepository.SubsPlanIntf, +) SubsPlanIntf { + return &SubsPlanSvc{subsAccs} +} diff --git a/internal/utilities/response/error_code.go b/internal/utilities/response/error_code.go index 6e8bb91..958bd00 100644 --- a/internal/utilities/response/error_code.go +++ b/internal/utilities/response/error_code.go @@ -8,8 +8,20 @@ type ErrorCode struct { HttpCode int } +func (e ErrorCode) Error() string { + return e.Code +} + var ( // 4xx - ErrBadRequest = ErrorCode{Code: "BAD_REQUEST", Message: "BAD_REQUEST", HttpCode: http.StatusBadRequest} - ErrDBRequest = ErrorCode{Code: "BAD_DB_REQUEST", Message: "DB_ERROR", HttpCode: http.StatusBadRequest} + ErrBadRequest = &ErrorCode{Code: "BAD_REQUEST", Message: "BAD_REQUEST", HttpCode: http.StatusBadRequest} + ErrDBRequest = &ErrorCode{Code: "BAD_DB_REQUEST", Message: "DB_ERROR", HttpCode: http.StatusBadRequest} + + // 5xx + ErrMarshal = &ErrorCode{Code: "FAILED_MARSHAL", Message: "FAILED_MARSHAL_BODY", HttpCode: http.StatusInternalServerError} + ErrCreateEntity = &ErrorCode{Code: "FAILED_CREATE_ENTITY", Message: "FAILED_CREATE_ENTITY", HttpCode: http.StatusInternalServerError} + + // redis + ErrRedisConnNotFound = &ErrorCode{Code: "FAILED_CONNECT_REDIS", Message: "REDIS_NOT_FOUND", HttpCode: http.StatusInternalServerError} + ErrRedisSet = &ErrorCode{Code: "FAILED_SET_REDIS", Message: "FAILED_CACHING", HttpCode: http.StatusInternalServerError} ) diff --git a/internal/utilities/utils/jwt.go b/internal/utilities/utils/jwt.go index 2d1e757..22787ec 100644 --- a/internal/utilities/utils/jwt.go +++ b/internal/utilities/utils/jwt.go @@ -3,7 +3,6 @@ package utils import ( "time" - jwtclaimenum "github.com/ardeman/project-legalgo-go/internal/enums/jwt" timeutils "github.com/ardeman/project-legalgo-go/internal/utilities/time_utils" "github.com/golang-jwt/jwt/v5" ) @@ -12,23 +11,23 @@ var jwtSecret = []byte("secret jwt key") // TODO: change later from env type ClaimOption func(options jwt.MapClaims) -func GenerateToken(options ...ClaimOption) (string, error) { - now := timeutils.Now() +// func GenerateToken(options ...ClaimOption) (string, error) { +// now := timeutils.Now() - claims := jwt.MapClaims{ - string(jwtclaimenum.ISSUED_AT): now.Unix(), - } +// claims := jwt.MapClaims{ +// string(jwtclaimenum.ISSUED_AT): now.Unix(), +// } - for _, o := range options { - o(claims) - } +// for _, o := range options { +// o(claims) +// } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) +// token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(jwtSecret) -} +// return token.SignedString(jwtSecret) +// } -func GenerateToken2(email string) (string, error) { +func GenerateToken(email string) (string, error) { now := timeutils.Now() token := jwt.New(jwt.SigningMethodHS256) claims := token.Claims.(jwt.MapClaims)