diff --git a/modules/user/controller/user_controller.go b/modules/user/controller/user_controller.go index 09b2cae..401622a 100644 --- a/modules/user/controller/user_controller.go +++ b/modules/user/controller/user_controller.go @@ -3,6 +3,7 @@ package controller import ( "net/http" + // "github.com/Caknoooo/go-gin-clean-starter/database/entities" authDto "github.com/Caknoooo/go-gin-clean-starter/modules/auth/dto" "github.com/Caknoooo/go-gin-clean-starter/modules/user/dto" "github.com/Caknoooo/go-gin-clean-starter/modules/user/query" @@ -68,22 +69,24 @@ func (c *userController) GetAllUser(ctx *gin.Context) { logrus.Info("Client ID: ", clientId) var filter = &query.UserFilter{ ClientID: clientId, - Name: ctx.Query("name"), // example additional filter + Name: ctx.Query("name"), + Includes: []string{"Roles"}, // Tambahkan ini } - logrus.Info("Filter: ", filter) filter.BindPagination(ctx) ctx.ShouldBindQuery(filter) - + logrus.Info("Filter sebelum query: ") users, total, err := pagination.PaginatedQueryWithIncludable[query.M_User](c.db, filter) + // logrus.Info("Filter: ", filter) if err != nil { res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_USER, err.Error(), nil) ctx.JSON(http.StatusBadRequest, res) return } - + // Convert ke DTO + userResponses := ToUserResponses(users) paginationResponse := pagination.CalculatePagination(filter.Pagination, total) - response := pagination.NewPaginatedResponse(http.StatusOK, dto.MESSAGE_SUCCESS_GET_LIST_USER, users, paginationResponse) + response := pagination.NewPaginatedResponse(http.StatusOK, dto.MESSAGE_SUCCESS_GET_LIST_USER, userResponses, paginationResponse) ctx.JSON(http.StatusOK, response) } @@ -223,3 +226,35 @@ func (c *userController) Refresh(ctx *gin.Context) { res := utils.BuildResponseSuccess(authDto.MESSAGE_SUCCESS_REFRESH_TOKEN, result) ctx.JSON(http.StatusOK, res) } + +// Function untuk convert dari M_User ke UserResponse +func ToUserResponse(user query.M_User) dto.UserResponse { + var roles []dto.UserRolesResponse + for _, role := range user.Roles { + roles = append(roles, dto.UserRolesResponse{ + ID: role.ID.String(), // pastikan role.ID bertipe uuid/atau string + Name: role.Name, + }) + } + + return dto.UserResponse{ + ID: user.ID, + Name: user.Name, + Username: user.Username, + Gender: user.Gender, + Address: user.Address, + Phone: user.Phone, + Email: user.Email, + PhotoUrl: user.PhotoUrl, + Roles: roles, + } +} + +// Function untuk convert slice of M_User +func ToUserResponses(users []query.M_User) []dto.UserResponse { + var responses []dto.UserResponse + for _, user := range users { + responses = append(responses, ToUserResponse(user)) + } + return responses +} diff --git a/modules/user/dto/user_dto.go b/modules/user/dto/user_dto.go index aec66a9..5ac4e0c 100644 --- a/modules/user/dto/user_dto.go +++ b/modules/user/dto/user_dto.go @@ -64,15 +64,21 @@ type ( } UserResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Username string `json:"username"` - Password string `json:"password,omitempty"` - Gender string `json:"gender"` - Address string `json:"address"` - Phone string `json:"phone"` - Email string `json:"email"` - PhotoUrl string `json:"photo_url"` + ID string `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + Gender string `json:"gender"` + Address string `json:"address"` + Phone string `json:"phone"` + Email string `json:"email"` + PhotoUrl string `json:"photo_url"` + Roles []UserRolesResponse `json:"roles,omitempty"` + } + + UserRolesResponse struct { + ID string `json:"id"` + Name string `json:"name"` } UserPaginationResponse struct { @@ -81,7 +87,7 @@ type ( } GetAllUserRepositoryResponse struct { - Users []entities.User `json:"users"` + Users []entities.M_User `json:"users"` dto.PaginationResponse } @@ -127,4 +133,8 @@ type ( Email string `json:"email" form:"email" binding:"required"` Password string `json:"password" form:"password" binding:"required"` } + + SwitchRoleRequest struct { + RoleID string `json:"role_id" form:"role_id" binding:"required,uuid4"` + } ) diff --git a/modules/user/query/user_query.go b/modules/user/query/user_query.go index 0d19680..7802551 100644 --- a/modules/user/query/user_query.go +++ b/modules/user/query/user_query.go @@ -1,6 +1,7 @@ package query import ( + "github.com/Caknoooo/go-gin-clean-starter/database/entities" "github.com/Caknoooo/go-pagination" "gorm.io/gorm" ) @@ -15,12 +16,15 @@ type M_User struct { Phone string `json:"phone"` Email string `json:"email"` PhotoUrl string `json:"photo_url"` + // Roles []entities.M_Role `json:"roles" gorm:"many2many:m_user_roles;"` + Roles []entities.M_Role `gorm:"many2many:m_user_roles;foreignKey:ID;joinForeignKey:UserID;References:ID;JoinReferences:RoleID" json:"roles"` } type UserFilter struct { pagination.BaseFilter - Name string `form:"name"` // tambahkan ini - ClientID string `form:"client_id"` // tambahkan ini + Name string `form:"name"` // tambahkan ini + ClientID string `form:"client_id"` // tambahkan ini + Includes []string `form:"includes"` // tambahkan ini } func (f *UserFilter) ApplyFilters(query *gorm.DB) *gorm.DB { @@ -31,6 +35,15 @@ func (f *UserFilter) ApplyFilters(query *gorm.DB) *gorm.DB { if f.ClientID != "" { query = query.Where("client_id = ?", f.ClientID) } + + // Manual preload untuk roles dengan field terbatas + // for _, include := range f.Includes { + // if include == "Roles" { + // query = query.Preload("Roles", func(db *gorm.DB) *gorm.DB { + // return db.Select("id", "name") // Hanya ambil id dan name + // }) + // } + // } return query } @@ -66,5 +79,7 @@ func (f *UserFilter) Validate() { } func (f *UserFilter) GetAllowedIncludes() map[string]bool { - return map[string]bool{} + return map[string]bool{ + "Roles": true, + } } diff --git a/modules/user/repository/user_repository.go b/modules/user/repository/user_repository.go index aaaca4f..8903a3b 100644 --- a/modules/user/repository/user_repository.go +++ b/modules/user/repository/user_repository.go @@ -15,6 +15,7 @@ type ( CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, bool, error) Update(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) Delete(ctx context.Context, tx *gorm.DB, userId string) error + SwitchRole(ctx context.Context, tx *gorm.DB, userId string, roleId string) (entities.M_User, error) } userRepository struct { @@ -28,6 +29,69 @@ func NewUserRepository(db *gorm.DB) UserRepository { } } +// SwitchRole implements UserRepository. +func (r *userRepository) SwitchRole(ctx context.Context, tx *gorm.DB, userId string, roleId string) (entities.M_User, error) { + if tx == nil { + tx = r.db + } + var user entities.M_User + // Preload UserRoles dan Role di dalamnya + if err := tx.WithContext(ctx). + Where("id = ?", userId). + Preload("UserRoles.Role"). + Take(&user).Error; err != nil { + return entities.M_User{}, err + } + var role entities.M_Role + if err := tx.WithContext(ctx).Where("id = ?", roleId).Take(&role).Error; err != nil { + return entities.M_User{}, err + } + // Ganti semua role user dengan role baru (hapus yang lama, insert yang baru) + if err := tx.WithContext(ctx). + Where("user_id = ?", userId). + Delete(&entities.M_User_Role{}).Error; err != nil { + return entities.M_User{}, err + } + newUserRole := entities.M_User_Role{ + UserID: user.ID, + RoleID: role.ID, + } + if err := tx.WithContext(ctx).Create(&newUserRole).Error; err != nil { + return entities.M_User{}, err + } + // Ambil ulang user beserta roles-nya + if err := tx.WithContext(ctx). + Where("id = ?", userId). + Preload("UserRoles.Role"). + Take(&user).Error; err != nil { + return entities.M_User{}, err + } + return user, nil +} + +// func (r *userRepository) SwitchRole(ctx context.Context, tx *gorm.DB, userId string, roleId string) (entities.M_User, error) { +// if tx == nil { +// tx = r.db +// } +// var user entities.M_User +// if err := tx.WithContext(ctx).Where("id = ?", userId).Take(&user).Error; err != nil { +// return entities.M_User{}, err +// } +// // Update field ActiveRoleID +// user.ActiveRoleID = roleId +// if err := tx.WithContext(ctx).Save(&user).Error; err != nil { +// return entities.M_User{}, err +// } +// // Preload roles jika perlu +// if err := tx.WithContext(ctx). +// Where("id = ?", userId). +// Preload("UserRoles.Role"). +// Take(&user).Error; err != nil { +// return entities.M_User{}, err +// } +// return user, nil +// } + func (r *userRepository) Register(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) { if tx == nil { tx = r.db @@ -46,7 +110,10 @@ func (r *userRepository) GetUserById(ctx context.Context, tx *gorm.DB, userId st } var user entities.M_User - if err := tx.WithContext(ctx).Where("id = ?", userId).Take(&user).Error; err != nil { + if err := tx.WithContext(ctx). + Preload("Roles"). + Where("id = ?", userId). + Take(&user).Error; err != nil { return entities.M_User{}, err } @@ -59,7 +126,10 @@ func (r *userRepository) GetUserByEmail(ctx context.Context, tx *gorm.DB, email } var user entities.M_User - if err := tx.WithContext(ctx).Where("email = ?", email).Take(&user).Error; err != nil { + if err := tx.WithContext(ctx). + Preload("Roles"). + Where("email = ?", email). + Take(&user).Error; err != nil { return entities.M_User{}, err } diff --git a/modules/user/scheduler/scheduler.go b/modules/user/scheduler/scheduler.go index 7afd76a..a0cd9f8 100644 --- a/modules/user/scheduler/scheduler.go +++ b/modules/user/scheduler/scheduler.go @@ -13,7 +13,7 @@ func Start(db *gorm.DB) { c := cron.New() // Setiap hari jam 00:00 _, err := c.AddFunc("0 0 * * *", func() { - if err := db.Model(&entities.User{}).Where("is_verified = ?", true).Update("is_verified", false).Error; err != nil { + if err := db.Model(&entities.M_User{}).Where("is_verified = ?", true).Update("is_verified", false).Error; err != nil { log.Println("Failed to update user verification:", err) } else { log.Println("All users set is_verified to false at", time.Now()) diff --git a/modules/user/service/user_service.go b/modules/user/service/user_service.go index 58f5d72..a17fff4 100644 --- a/modules/user/service/user_service.go +++ b/modules/user/service/user_service.go @@ -2,6 +2,7 @@ package service import ( "context" + "errors" "fmt" "github.com/Caknoooo/go-gin-clean-starter/database/entities" @@ -24,6 +25,7 @@ type UserService interface { Update(ctx context.Context, req dto.UserUpdateRequest, userId string) (dto.UserUpdateResponse, error) Delete(ctx context.Context, userId string) error RefreshToken(ctx context.Context, req authDto.RefreshTokenRequest) (authDto.TokenResponse, error) + SwitchRole(ctx context.Context, req dto.SwitchRoleRequest) (authDto.TokenResponse, error) } type userService struct { @@ -47,6 +49,11 @@ func NewUserService( } } +// SwitchRole implements UserService. +func (s *userService) SwitchRole(ctx context.Context, req dto.SwitchRoleRequest) (authDto.TokenResponse, error) { + panic("unimplemented") +} + func (s *userService) Register(ctx context.Context, req dto.UserCreateRequest) (dto.UserResponse, error) { _, exists, err := s.userRepository.CheckEmail(ctx, s.db, req.Email) if err != nil && err != gorm.ErrRecordNotFound { @@ -95,6 +102,14 @@ func (s *userService) GetUserById(ctx context.Context, userId string) (dto.UserR return dto.UserResponse{}, err } + var roles []dto.UserRolesResponse + for _, ur := range user.Roles { + roles = append(roles, dto.UserRolesResponse{ + ID: ur.ID.String(), + Name: ur.Name, + }) + } + return dto.UserResponse{ ID: user.ID.String(), Name: user.Name, @@ -104,6 +119,7 @@ func (s *userService) GetUserById(ctx context.Context, userId string) (dto.UserR Address: user.Address, Phone: user.Phone, PhotoUrl: user.PhotoUrl, + Roles: roles, }, nil } @@ -118,14 +134,25 @@ func (s *userService) Verify(ctx context.Context, req dto.UserLoginRequest) (aut fmt.Println("Password validation error:", err) return authDto.TokenResponse{}, dto.ErrUserNotFound } + // Ambil roles dari UserRoles + roles := append([]entities.M_Role{}, user.Roles...) + // var roles []dto.UserRolesResponse + // for _, ur := range user.Roles { + // roles = append(roles, dto.UserRolesResponse{ + // ID: ur.ID.String(), + // Name: ur.Name, + // }) + // } + if len(roles) == 0 { + return authDto.TokenResponse{}, errors.New("user has no roles assigned") + } - accessToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String()) + accessToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String(), roles[0].ID.String()) refreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken() refreshToken := entities.RefreshToken{ - ID: uuid.New(), - UserID: user.ID, - // TenantID: user.TenantID, + ID: uuid.New(), + UserID: user.ID, ClientID: user.ClientID, Token: refreshTokenString, ExpiresAt: expiresAt, @@ -152,7 +179,12 @@ func (s *userService) SendVerificationEmail(ctx context.Context, req dto.SendVer // return dto.ErrAccountAlreadyVerified // } - verificationToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String()) + roles := append([]entities.M_Role{}, user.Roles...) + if len(roles) == 0 { + return errors.New("user has no roles assigned") + } + + verificationToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String(), roles[0].ID.String()) subject := "Email Verification" body := "Please verify your email using this token: " + verificationToken @@ -258,8 +290,12 @@ func (s *userService) RefreshToken(ctx context.Context, req authDto.RefreshToken if err != nil { return authDto.TokenResponse{}, err } + roles := append([]entities.M_Role{}, refreshToken.User.Roles...) + if len(roles) == 0 { + return authDto.TokenResponse{}, errors.New("user has no roles assigned") + } - accessToken := s.jwtService.GenerateAccessToken(refreshToken.ClientID.String(), refreshToken.UserID.String()) + accessToken := s.jwtService.GenerateAccessToken(refreshToken.ClientID.String(), refreshToken.UserID.String(), roles[0].ID.String()) newRefreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken() err = s.refreshTokenRepository.DeleteByToken(ctx, s.db, req.RefreshToken)