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 (
"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
}

View File

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

View File

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

View File

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

View File

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

View File

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