Compare commits

..

2 Commits

Author SHA1 Message Date
ericprd
bd305f0edf docs: update setup database 2025-02-24 16:53:18 +08:00
ericprd
cb3d06fa02 feat: CR login, register, subscribe 2025-02-24 16:48:20 +08:00
50 changed files with 1058 additions and 59 deletions

View File

@ -3,3 +3,10 @@ DB_USER=
DB_PASSWORD= DB_PASSWORD=
DB_NAME= DB_NAME=
DB_PORT= DB_PORT=
REDIS_HOST=
REDIS_USERNAME=
REDIS_PORT=
REDIS_PASSWORD=
REDIS_TTL=
REDIS_DB=

View File

@ -76,6 +76,30 @@ Run the following command to download and resolve all dependencies specified in
go mod tidy go mod tidy
``` ```
## Setup Database
In your Go application, you'll need to set the following environment variables to connect to your database:
```bash
DB_HOST: The hostname or IP address of your database server (e.g., localhost, 127.0.0.1, or the actual DB server URL).
DB_USER: The username for your database connection.
DB_PASSWORD: The password associated with the DB_USER.
DB_NAME: The name of the database you want to connect to.
DB_PORT: The port number your database server is running on (default MySQL port is 3306, default PostgreSQL port is 5432).
```
## Create `.env` File
It's best practice to keep sensitive information like database credentials in an environment file. Create or edit the .env file in the root directory of your project and add the following lines:
```bash
DB_HOST=localhost
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_NAME=your_db_name
DB_PORT=3306 # or 5432, depending on your DB type
Make sure to replace the values with your actual database details.
```
## Build the Binary Using `make build` ## Build the Binary Using `make build`
Run the following command to build your project. The binary will be placed in the `bin` folder. Run the following command to build your project. The binary will be placed in the `bin` folder.

View File

@ -5,6 +5,8 @@ import (
"os" "os"
staffmodel "github.com/ardeman/project-legalgo-go/database/staff" 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/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -39,5 +41,10 @@ func NewDB() (*DB, error) {
func (db *DB) Migrate() error { func (db *DB) Migrate() error {
// Auto Migrate the User model // Auto Migrate the User model
return db.AutoMigrate(&staffmodel.Staff{}) return db.AutoMigrate(
&staffmodel.Staff{},
&subsmodel.SubscribePlan{},
&subsmodel.Subscribe{},
&usermodel.User{},
)
} }

View File

@ -1,6 +1,8 @@
package staffmodel package staffmodel
import ( import (
"time"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -8,4 +10,6 @@ type Staff struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"` ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
Email string `gorm:"unique,not null" json:"email"` Email string `gorm:"unique,not null" json:"email"`
Password string `gorm:"not null" json:"password"` 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"`
} }

View File

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

19
database/user/model.go Normal file
View File

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

View File

@ -1,10 +1,18 @@
package repository package repository
import ( import (
redisaccessor "github.com/ardeman/project-legalgo-go/internal/accessor/redis"
staffrepository "github.com/ardeman/project-legalgo-go/internal/accessor/staff" 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" "go.uber.org/fx"
) )
var Module = fx.Module("repository", fx.Provide( var Module = fx.Module("repository", fx.Provide(
staffrepository.New, staffrepository.New,
userrepository.New,
redisaccessor.New,
subscribeplanrepository.New,
subscriberepository.New,
)) ))

View File

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

View File

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

View File

@ -3,12 +3,12 @@ package staffrepository
import ( import (
"errors" "errors"
staffmodel "github.com/ardeman/project-legalgo-go/database/staff" authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth"
"gorm.io/gorm" "gorm.io/gorm"
) )
func (sr *StaffRepository) GetStaff(email string) (*staffmodel.Staff, error) { func (sr *StaffRepository) GetStaffByEmail(email string) (*authdomain.LoginRepoResponse, error) {
var staff staffmodel.Staff var staff authdomain.LoginRepoResponse
if email == "" { if email == "" {
return nil, errors.New("email is empty") return nil, errors.New("email is empty")

View File

@ -2,17 +2,18 @@ package staffrepository
import ( import (
"github.com/ardeman/project-legalgo-go/database" "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 { type StaffRepository struct {
DB *database.DB DB *database.DB
} }
type StaffInterface interface { type StaffIntf interface {
GetStaff(string) (*staffmodel.Staff, error) 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} return &StaffRepository{db}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,8 @@ package authhttp
import ( import (
"net/http" "net/http"
domain "github.com/ardeman/project-legalgo-go/internal/domain/auth" authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth"
serviceauth "github.com/ardeman/project-legalgo-go/internal/services/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/response"
"github.com/ardeman/project-legalgo-go/internal/utilities/utils" "github.com/ardeman/project-legalgo-go/internal/utilities/utils"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -13,15 +13,15 @@ import (
func LoginStaff( func LoginStaff(
router chi.Router, router chi.Router,
authSvc serviceauth.LoginStaffIntf, authSvc authsvc.AuthIntf,
validate *validator.Validate, validate *validator.Validate,
) { ) {
router.Post("/staff/login", func(w http.ResponseWriter, r *http.Request) { router.Post("/staff/login", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() 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( response.ResponseWithErrorCode(
ctx, ctx,
w, w,
@ -33,7 +33,7 @@ func LoginStaff(
return return
} }
if err := validate.Struct(request); err != nil { if err := validate.Struct(spec); err != nil {
response.ResponseWithErrorCode( response.ResponseWithErrorCode(
ctx, ctx,
w, w,
@ -45,7 +45,7 @@ func LoginStaff(
return return
} }
token, err := authSvc.LoginAsStaff(request.Email) token, err := authSvc.LoginAsStaff(spec)
if err != nil { if err != nil {
response.ResponseWithErrorCode( response.ResponseWithErrorCode(
ctx, ctx,
@ -58,7 +58,62 @@ func LoginStaff(
return 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, Token: token,
} }

View File

@ -5,5 +5,8 @@ import "go.uber.org/fx"
var Module = fx.Module("auth-api", var Module = fx.Module("auth-api",
fx.Invoke( fx.Invoke(
LoginStaff, LoginStaff,
LoginUser,
RegisterUser,
RegisterStaff,
), ),
) )

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package internalhttp
import ( import (
authhttp "github.com/ardeman/project-legalgo-go/internal/api/http/auth" 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/chi/v5"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@ -16,6 +17,7 @@ var Module = fx.Module("router",
validator.New, validator.New,
), ),
authhttp.Module, authhttp.Module,
subscribeplanhttp.Module,
) )
func initRouter() chi.Router { func initRouter() chi.Router {

View File

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

View File

@ -0,0 +1,9 @@
package subscribeplanhttp
import "go.uber.org/fx"
var Module = fx.Module("subscribe-plan",
fx.Invoke(
CreateSubscribePlan,
),
)

View File

@ -16,7 +16,7 @@ import (
func Router(apiRouter chi.Router) { func Router(apiRouter chi.Router) {
mainRouter := chi.NewRouter() mainRouter := chi.NewRouter()
mainRouter.Mount("/", apiRouter) mainRouter.Mount("/api", apiRouter)
mainCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) mainCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop() defer stop()

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
package cachingdomain
import "time"
type CacheSpec struct {
Key string
TTL time.Duration
Data any
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@
package contextkeyenum
type ContextKey string
const (
Authorization ContextKey = "AUTHORIZATION_CONTEXT"
Resource ContextKey = "RESOURCE_CONTEXT"
)

View File

@ -8,4 +8,5 @@ const (
EXPIRED_AT JWTClaim = "exp" EXPIRED_AT JWTClaim = "exp"
SESSION_ID JWTClaim = "sid" SESSION_ID JWTClaim = "sid"
ISSUED_AT JWTClaim = "iat" ISSUED_AT JWTClaim = "iat"
RESOURCES JWTClaim = "resources"
) )

View File

@ -0,0 +1,3 @@
package resourceenum
type Resource string

View File

@ -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 { type AuthSvc struct {
staffAcs staffrepository.StaffInterface staffRepo staffrepository.StaffIntf
userRepo userrepository.UserIntf
subsRepo subscriberepository.SubsIntf
} }
type LoginStaffIntf interface { type AuthIntf interface {
LoginAsStaff(string) (string, error) LoginAsStaff(authdomain.LoginReq) (string, error)
LoginAsUser(authdomain.LoginReq) (string, error)
RegisterUser(authdomain.RegisterUserReq) (string, error)
RegisterStaff(authdomain.RegisterStaffReq) (string, error)
} }
func New( func New(
staffAcs staffrepository.StaffInterface, staffRepo staffrepository.StaffIntf,
) LoginStaffIntf { userRepo userrepository.UserIntf,
return &LoginStaffSvc{ subsRepo subscriberepository.SubsIntf,
staffAcs: staffAcs, ) AuthIntf {
return &AuthSvc{
staffRepo: staffRepo,
userRepo: userRepo,
subsRepo: subsRepo,
} }
} }

View File

@ -1,18 +1,24 @@
package serviceauth package authsvc
import ( import (
"errors" "errors"
authdomain "github.com/ardeman/project-legalgo-go/internal/domain/auth"
"github.com/ardeman/project-legalgo-go/internal/utilities/utils" "github.com/ardeman/project-legalgo-go/internal/utilities/utils"
) )
func (sv *LoginStaffSvc) LoginAsStaff(email string) (string, error) { func (sv *AuthSvc) LoginAsStaff(spec authdomain.LoginReq) (string, error) {
staff, err := sv.staffAcs.GetStaff(email) staff, err := sv.staffRepo.GetStaffByEmail(spec.Email)
if err != nil { if err != nil {
return "", errors.New(err.Error()) 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 { if err != nil {
return "", errors.New(err.Error()) return "", errors.New(err.Error())
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,11 +2,15 @@ package services
import ( import (
serviceauth "github.com/ardeman/project-legalgo-go/internal/services/auth" 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" "go.uber.org/fx"
) )
var Module = fx.Module("services", var Module = fx.Module("services",
fx.Provide( fx.Provide(
serviceauth.New, serviceauth.New,
subscribeplansvc.New,
subscribesvc.New,
), ),
) )

View File

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

View File

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

View File

@ -0,0 +1,5 @@
package subscribeplansvc
func (sb *SubsPlanSvc) CreatePlan(code string) error {
return sb.subsAccs.Create(code)
}

View File

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

View File

@ -8,8 +8,20 @@ type ErrorCode struct {
HttpCode int HttpCode int
} }
func (e ErrorCode) Error() string {
return e.Code
}
var ( var (
// 4xx // 4xx
ErrBadRequest = ErrorCode{Code: "BAD_REQUEST", Message: "BAD_REQUEST", 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} 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}
) )

View File

@ -3,7 +3,6 @@ package utils
import ( import (
"time" "time"
jwtclaimenum "github.com/ardeman/project-legalgo-go/internal/enums/jwt"
timeutils "github.com/ardeman/project-legalgo-go/internal/utilities/time_utils" timeutils "github.com/ardeman/project-legalgo-go/internal/utilities/time_utils"
"github.com/golang-jwt/jwt/v5" "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) type ClaimOption func(options jwt.MapClaims)
func GenerateToken(options ...ClaimOption) (string, error) { // func GenerateToken(options ...ClaimOption) (string, error) {
now := timeutils.Now() // now := timeutils.Now()
claims := jwt.MapClaims{ // claims := jwt.MapClaims{
string(jwtclaimenum.ISSUED_AT): now.Unix(), // string(jwtclaimenum.ISSUED_AT): now.Unix(),
} // }
for _, o := range options { // for _, o := range options {
o(claims) // 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() now := timeutils.Now()
token := jwt.New(jwt.SigningMethodHS256) token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims) claims := token.Claims.(jwt.MapClaims)