Implement user role management and update user response structure to include roles

This commit is contained in:
Habib Fatkhul Rohman 2025-10-21 15:04:34 +07:00
parent 8357303237
commit f28b3a5244
6 changed files with 193 additions and 27 deletions

View File

@ -3,6 +3,7 @@ package controller
import ( import (
"net/http" "net/http"
// "github.com/Caknoooo/go-gin-clean-starter/database/entities"
authDto "github.com/Caknoooo/go-gin-clean-starter/modules/auth/dto" 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/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/user/query" "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) logrus.Info("Client ID: ", clientId)
var filter = &query.UserFilter{ var filter = &query.UserFilter{
ClientID: clientId, 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) filter.BindPagination(ctx)
ctx.ShouldBindQuery(filter) ctx.ShouldBindQuery(filter)
logrus.Info("Filter sebelum query: ")
users, total, err := pagination.PaginatedQueryWithIncludable[query.M_User](c.db, filter) users, total, err := pagination.PaginatedQueryWithIncludable[query.M_User](c.db, filter)
// logrus.Info("Filter: ", filter)
if err != nil { if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_USER, err.Error(), nil) res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_USER, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res) ctx.JSON(http.StatusBadRequest, res)
return return
} }
// Convert ke DTO
userResponses := ToUserResponses(users)
paginationResponse := pagination.CalculatePagination(filter.Pagination, total) 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) 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) res := utils.BuildResponseSuccess(authDto.MESSAGE_SUCCESS_REFRESH_TOKEN, result)
ctx.JSON(http.StatusOK, res) 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
}

View File

@ -73,6 +73,12 @@ type (
Phone string `json:"phone"` Phone string `json:"phone"`
Email string `json:"email"` Email string `json:"email"`
PhotoUrl string `json:"photo_url"` PhotoUrl string `json:"photo_url"`
Roles []UserRolesResponse `json:"roles,omitempty"`
}
UserRolesResponse struct {
ID string `json:"id"`
Name string `json:"name"`
} }
UserPaginationResponse struct { UserPaginationResponse struct {
@ -81,7 +87,7 @@ type (
} }
GetAllUserRepositoryResponse struct { GetAllUserRepositoryResponse struct {
Users []entities.User `json:"users"` Users []entities.M_User `json:"users"`
dto.PaginationResponse dto.PaginationResponse
} }
@ -127,4 +133,8 @@ type (
Email string `json:"email" form:"email" binding:"required"` Email string `json:"email" form:"email" binding:"required"`
Password string `json:"password" form:"password" binding:"required"` Password string `json:"password" form:"password" binding:"required"`
} }
SwitchRoleRequest struct {
RoleID string `json:"role_id" form:"role_id" binding:"required,uuid4"`
}
) )

View File

@ -1,6 +1,7 @@
package query package query
import ( import (
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"github.com/Caknoooo/go-pagination" "github.com/Caknoooo/go-pagination"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -15,12 +16,15 @@ type M_User struct {
Phone string `json:"phone"` Phone string `json:"phone"`
Email string `json:"email"` Email string `json:"email"`
PhotoUrl string `json:"photo_url"` 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 { type UserFilter struct {
pagination.BaseFilter pagination.BaseFilter
Name string `form:"name"` // tambahkan ini Name string `form:"name"` // tambahkan ini
ClientID string `form:"client_id"` // tambahkan ini ClientID string `form:"client_id"` // tambahkan ini
Includes []string `form:"includes"` // tambahkan ini
} }
func (f *UserFilter) ApplyFilters(query *gorm.DB) *gorm.DB { 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 != "" { if f.ClientID != "" {
query = query.Where("client_id = ?", 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 return query
} }
@ -66,5 +79,7 @@ func (f *UserFilter) Validate() {
} }
func (f *UserFilter) GetAllowedIncludes() map[string]bool { func (f *UserFilter) GetAllowedIncludes() map[string]bool {
return map[string]bool{} return map[string]bool{
"Roles": true,
}
} }

View File

@ -15,6 +15,7 @@ type (
CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, bool, error) 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) 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 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 { 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) { func (r *userRepository) Register(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) {
if tx == nil { if tx == nil {
tx = r.db tx = r.db
@ -46,7 +110,10 @@ func (r *userRepository) GetUserById(ctx context.Context, tx *gorm.DB, userId st
} }
var user entities.M_User 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 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 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 return entities.M_User{}, err
} }

View File

@ -13,7 +13,7 @@ func Start(db *gorm.DB) {
c := cron.New() c := cron.New()
// Setiap hari jam 00:00 // Setiap hari jam 00:00
_, err := c.AddFunc("0 0 * * *", func() { _, 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) log.Println("Failed to update user verification:", err)
} else { } else {
log.Println("All users set is_verified to false at", time.Now()) log.Println("All users set is_verified to false at", time.Now())

View File

@ -2,6 +2,7 @@ package service
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/Caknoooo/go-gin-clean-starter/database/entities" "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) Update(ctx context.Context, req dto.UserUpdateRequest, userId string) (dto.UserUpdateResponse, error)
Delete(ctx context.Context, userId string) error Delete(ctx context.Context, userId string) error
RefreshToken(ctx context.Context, req authDto.RefreshTokenRequest) (authDto.TokenResponse, error) RefreshToken(ctx context.Context, req authDto.RefreshTokenRequest) (authDto.TokenResponse, error)
SwitchRole(ctx context.Context, req dto.SwitchRoleRequest) (authDto.TokenResponse, error)
} }
type userService struct { 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) { func (s *userService) Register(ctx context.Context, req dto.UserCreateRequest) (dto.UserResponse, error) {
_, exists, err := s.userRepository.CheckEmail(ctx, s.db, req.Email) _, exists, err := s.userRepository.CheckEmail(ctx, s.db, req.Email)
if err != nil && err != gorm.ErrRecordNotFound { 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 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{ return dto.UserResponse{
ID: user.ID.String(), ID: user.ID.String(),
Name: user.Name, Name: user.Name,
@ -104,6 +119,7 @@ func (s *userService) GetUserById(ctx context.Context, userId string) (dto.UserR
Address: user.Address, Address: user.Address,
Phone: user.Phone, Phone: user.Phone,
PhotoUrl: user.PhotoUrl, PhotoUrl: user.PhotoUrl,
Roles: roles,
}, nil }, nil
} }
@ -118,14 +134,25 @@ func (s *userService) Verify(ctx context.Context, req dto.UserLoginRequest) (aut
fmt.Println("Password validation error:", err) fmt.Println("Password validation error:", err)
return authDto.TokenResponse{}, dto.ErrUserNotFound 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() refreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken()
refreshToken := entities.RefreshToken{ refreshToken := entities.RefreshToken{
ID: uuid.New(), ID: uuid.New(),
UserID: user.ID, UserID: user.ID,
// TenantID: user.TenantID,
ClientID: user.ClientID, ClientID: user.ClientID,
Token: refreshTokenString, Token: refreshTokenString,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
@ -152,7 +179,12 @@ func (s *userService) SendVerificationEmail(ctx context.Context, req dto.SendVer
// return dto.ErrAccountAlreadyVerified // 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" subject := "Email Verification"
body := "Please verify your email using this token: " + verificationToken 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 { if err != nil {
return authDto.TokenResponse{}, err 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() newRefreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken()
err = s.refreshTokenRepository.DeleteByToken(ctx, s.db, req.RefreshToken) err = s.refreshTokenRepository.DeleteByToken(ctx, s.db, req.RefreshToken)