package service import ( "context" "github.com/Caknoooo/go-gin-clean-starter/database/entities" authRepo "github.com/Caknoooo/go-gin-clean-starter/modules/auth/repository" "github.com/Caknoooo/go-gin-clean-starter/modules/auth/service" "github.com/Caknoooo/go-gin-clean-starter/modules/role/dto" "github.com/Caknoooo/go-gin-clean-starter/modules/role/query" "github.com/Caknoooo/go-gin-clean-starter/modules/role/repository" userDto "github.com/Caknoooo/go-gin-clean-starter/modules/user/dto" userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service" pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" "github.com/google/uuid" "github.com/sirupsen/logrus" "gorm.io/gorm" "gorm.io/gorm/clause" ) type RoleService interface { CreateRole(ctx context.Context, role dto.RoleCreateRequest) (dto.RoleResponse, error) GetRoles(ctx context.Context, filter query.RoleFilter) ([]dto.RoleResponse, error) GetRoleByID(ctx context.Context, id string) (dto.RoleResponse, error) UpdateRole(ctx context.Context, id string, role dto.RoleUpdateRequest) (dto.RoleResponse, error) DeleteRole(ctx context.Context, id string) error AssignPermissionsToRole(ctx context.Context, roleId string, permissions []string) error RemovePermissionsFromRole(ctx context.Context, roleId string, permissions []string) error AssignRolesToUser(ctx context.Context, userId string, roleIds []string) error RemoveRolesFromUser(ctx context.Context, userId string, roleIds []string) error GetRolesByUserID(ctx context.Context, userId string) ([]dto.RoleResponse, error) AssignMenusToRole(ctx context.Context, roleId string, menuIds []string) error RemoveMenusFromRole(ctx context.Context, roleId string, menuIds []string) error GetAll(ctx context.Context) ([]dto.RoleResponse, error) } type roleService struct { roleRepo repository.RoleRepository refreshTokenRepository authRepo.RefreshTokenRepository jwtService service.JWTService userService userService.UserService db *gorm.DB } // GetAll implements RoleService. func (r *roleService) GetAll(ctx context.Context) ([]dto.RoleResponse, error) { roles, err := r.roleRepo.GetAll(ctx, r.db) if err != nil { return nil, err } var responses []dto.RoleResponse for _, role := range roles { responses = append(responses, ToRoleResponse(role)) } return responses, nil } // AssignMenusToRole implements RoleService. // AssignMenusToRole implements RoleService. func (r *roleService) AssignMenusToRole(ctx context.Context, roleId string, menuIds []string) error { if len(menuIds) == 0 { return nil } // Pastikan role ada if _, err := r.GetRoleByID(ctx, roleId); err != nil { return dto.ErrRoleNotFound } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // Validasi role di DB _, err := r.roleRepo.GetRoleByID(ctx, tx, roleId) if err != nil { return dto.ErrRoleNotFound } rows := make([]entities.M_Role_Menu, 0, len(menuIds)) roleUUID, err := uuid.Parse(roleId) if err != nil { return err } for _, mid := range menuIds { menuUUID, err := uuid.Parse(mid) if err != nil { return err } rows = append(rows, entities.M_Role_Menu{ RoleID: roleUUID, MenuID: menuUUID, }) } if len(rows) == 0 { return dto.ErrMenuNotFound } res := tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "role_id"}, {Name: "menu_id"}}, DoNothing: true, }).Create(&rows) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return dto.ErrMenuAlreadyExists } return nil }) } // RemoveMenusFromRole implements RoleService. func (r *roleService) RemoveMenusFromRole(ctx context.Context, roleId string, menuIds []string) error { if len(menuIds) == 0 { return nil } // Pastikan role ada if _, err := r.GetRoleByID(ctx, roleId); err != nil { return dto.ErrRoleNotFound } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // Validasi role di DB _, err := r.roleRepo.GetRoleByID(ctx, tx, roleId) if err != nil { return dto.ErrRoleNotFound } // Remove menus dari role if err := r.roleRepo.RemoveMenusFromRole(ctx, tx, roleId, menuIds); err != nil { return err } return nil }) } // AssignPermissionsToRole implements RoleService. func (r *roleService) AssignPermissionsToRole(ctx context.Context, roleId string, permission_ids []string) error { if len(permission_ids) == 0 { return nil } if _, err := r.GetRoleByID(ctx, roleId); err != nil { return dto.ErrRoleNotFound } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { _, err := r.roleRepo.GetRoleByID(ctx, tx, roleId) if err != nil { return dto.ErrRoleNotFound } rows := make([]entities.M_Role_Permission, 0, len(permission_ids)) roleUUID, err := uuid.Parse(roleId) if err != nil { return err } for _, pid := range permission_ids { permissionUUID, err := uuid.Parse(pid) if err != nil { return err } // _, err = r.roleRepo.GetPermissionByID(ctx, tx, pid) // if err != nil { // return dto.ErrPermissionNotFound // } rows = append(rows, entities.M_Role_Permission{ RoleID: roleUUID, PermissionID: permissionUUID, }) logrus.Info("Prepared to assign permission ", permissionUUID, " to role ", roleUUID) } if len(rows) == 0 { return dto.ErrPermissionNotFound } res := tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "role_id"}, {Name: "permission_id"}}, DoNothing: true, }).Create(&rows) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return dto.ErrPermissionAlreadyExists } return nil }) } // AssignRolesToUser implements RoleService. func (r *roleService) AssignRolesToUser(ctx context.Context, userId string, roleIds []string) error { if len(roleIds) == 0 { return nil } // verify user exists via UserService (reuse business logic) if _, err := r.userService.GetUserById(ctx, userId); err != nil { return userDto.ErrUserNotFound } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { rows := make([]entities.M_User_Role, 0, len(roleIds)) userUUID, err := uuid.Parse(userId) if err != nil { return err } for _, rid := range roleIds { roleUUID, err := uuid.Parse(rid) if err != nil { return err } _, err = r.roleRepo.GetRoleByID(ctx, tx, rid) if err != nil { continue } rows = append(rows, entities.M_User_Role{ UserID: userUUID, RoleID: roleUUID, }) } if len(rows) == 0 { return dto.ErrRoleNotFound } res := tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "user_id"}, {Name: "role_id"}}, DoNothing: true, }).Create(&rows) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return dto.ErrRoleAlreadyExists } return nil }) } // CreateRole implements RoleService. func (r *roleService) CreateRole(ctx context.Context, req dto.RoleCreateRequest) (dto.RoleResponse, error) { // _, exists, err := r.roleRepo.CheckRoleName(ctx, r.db, req.Name) // if err != nil && err != gorm.ErrRecordNotFound { // return dto.RoleResponse{}, err // } // if exists { // return dto.RoleResponse{}, dto.ErrRoleAlreadyExists // } clientUUID, err := uuid.Parse(req.ClientID) if err != nil { return dto.RoleResponse{}, err } role := entities.M_Role{ Name: req.Name, Description: req.Description, IconUrl: req.IconUrl, Type: req.Type, HomeUrl: req.HomeUrl, ClientID: clientUUID, } createdRole, err := r.roleRepo.CreateRole(ctx, r.db, role) if err != nil { return dto.RoleResponse{}, err } result, err := r.roleRepo.GetRoleByID(ctx, r.db, createdRole.ID.String()) if err != nil { return dto.RoleResponse{}, err } return ToRoleResponse(result), nil } // DeleteRole implements RoleService. func (r *roleService) DeleteRole(ctx context.Context, id string) error { if _, err := r.roleRepo.GetRoleByID(ctx, r.db, id); err != nil { return dto.ErrRoleNotFound } return r.roleRepo.DeleteRole(ctx, r.db, id) } // GetRoleByID implements RoleService. func (r *roleService) GetRoleByID(ctx context.Context, id string) (dto.RoleResponse, error) { role, err := r.roleRepo.GetRoleByID(ctx, r.db, id) if err != nil { return dto.RoleResponse{}, err } return ToRoleResponse(role), nil } // GetRoles implements RoleService. func (r *roleService) GetRoles(ctx context.Context, filter query.RoleFilter) ([]dto.RoleResponse, error) { panic("unimplemented") } // GetRolesByUserID implements RoleService. // func (r *roleService) GetRolesByUserID(ctx context.Context, userId string) ([]dto.RoleResponse, error) { // if _, err := r.userService.GetUserById(ctx, userId); err != nil { // return nil, userDto.ErrUserNotFound // } // roles, err := r.roleRepo.GetRolesByUserID(ctx, r.db, userId) // if err != nil { // logrus.Error("Error fetching roles for user ", userId, ": ", err) // return nil, err // } // logrus.Info("Fetched ", len(roles), " roles for user ", userId) // var responses []dto.RoleResponse // for _, role := range roles { // var permissions []dto.RolePermissionsResponse // for _, p := range role.Permissions { // permissions = append(permissions, dto.RolePermissionsResponse{ // ID: p.ID.String(), // Name: p.Name, // }) // } // responses = append(responses, dto.RoleResponse{ // ID: role.ID.String(), // Name: role.Name, // Description: role.Description, // IconUrl: role.IconUrl, // Type: role.Type, // HomeUrl: role.HomeUrl, // ClientID: role.ClientID.String(), // Permissions: permissions, // }) // } // return responses, nil // } func (r *roleService) GetRolesByUserID(ctx context.Context, userId string) ([]dto.RoleResponse, error) { if _, err := r.userService.GetUserById(ctx, userId); err != nil { return nil, userDto.ErrUserNotFound } roles, err := r.roleRepo.GetRolesByUserID(ctx, r.db, userId) if err != nil { return nil, err } var responses []dto.RoleResponse for _, role := range roles { responses = append(responses, ToRoleResponse(role)) } return responses, nil } // RemovePermissionsFromRole implements RoleService. func (r *roleService) RemovePermissionsFromRole(ctx context.Context, roleId string, permission_ids []string) error { if len(permission_ids) == 0 { return nil } if _, err := r.GetRoleByID(ctx, roleId); err != nil { return dto.ErrRoleNotFound } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { roleUUID, err := uuid.Parse(roleId) if err != nil { return err } permissionUUIDs := make([]uuid.UUID, 0, len(permission_ids)) for _, pid := range permission_ids { permissionUUID, err := uuid.Parse(pid) if err != nil { return err } permissionUUIDs = append(permissionUUIDs, permissionUUID) } if len(permissionUUIDs) == 0 { return dto.ErrPermissionNotFound } // delete matching user-role pairs res := tx.Where("role_id = ? AND permission_id IN ?", roleUUID, permissionUUIDs). Delete(&entities.M_Role_Permission{}) if res.Error != nil { return res.Error } // jika tidak ada row yang dihapus, kembalikan error agar klien tahu tidak ada role yang cocok if res.RowsAffected == 0 { return dto.ErrRoleNotFound } return nil }) } // RemoveRolesFromUser implements RoleService. func (r *roleService) RemoveRolesFromUser(ctx context.Context, userId string, roleIds []string) error { if len(roleIds) == 0 { return nil } // verify user exists via UserService (reuse business logic) if _, err := r.userService.GetUserById(ctx, userId); err != nil { return userDto.ErrUserNotFound } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { userUUID, err := uuid.Parse(userId) if err != nil { return err } roleUUIDs := make([]uuid.UUID, 0, len(roleIds)) for _, rid := range roleIds { roleUUID, err := uuid.Parse(rid) if err != nil { return err } roleUUIDs = append(roleUUIDs, roleUUID) } if len(roleUUIDs) == 0 { return dto.ErrRoleNotFound } // delete matching user-role pairs res := tx.Where("user_id = ? AND role_id IN ?", userUUID, roleUUIDs). Delete(&entities.M_User_Role{}) if res.Error != nil { return res.Error } // jika tidak ada row yang dihapus, kembalikan error agar klien tahu tidak ada role yang cocok if res.RowsAffected == 0 { return dto.ErrRoleNotFound } return nil }) } // UpdateRole implements RoleService. func (r *roleService) UpdateRole(ctx context.Context, id string, req dto.RoleUpdateRequest) (dto.RoleResponse, error) { existingRole, err := r.roleRepo.GetRoleByID(ctx, r.db, id) if err != nil { return dto.RoleResponse{}, dto.ErrRoleNotFound } if req.Name != "" { // _, exists, err := r.roleRepo.CheckRoleName(ctx, r.db, req.Name) // if err != nil { // return dto.RoleResponse{}, err // } // if exists { // return dto.RoleResponse{}, dto.ErrRoleAlreadyExists // } existingRole.Name = req.Name } if req.Description != "" { existingRole.Description = req.Description } if req.IconUrl != "" { existingRole.IconUrl = req.IconUrl } if req.Type != "" { existingRole.Type = req.Type } if req.HomeUrl != "" { existingRole.HomeUrl = req.HomeUrl } updatedRole, err := r.roleRepo.UpdateRole(ctx, r.db, existingRole) if err != nil { return dto.RoleResponse{}, err } result, err := r.roleRepo.GetRoleByID(ctx, r.db, updatedRole.ID.String()) if err != nil { return dto.RoleResponse{}, err } return ToRoleResponse(result), nil } func ToRoleResponse(role entities.M_Role) dto.RoleResponse { var permissions []dto.RolePermissionsResponse for _, p := range role.Permissions { permissions = append(permissions, dto.RolePermissionsResponse{ ID: p.ID.String(), Name: p.Name, }) } var client pkgdto.IdNameResponse if role.Client.ID != uuid.Nil { client = pkgdto.IdNameResponse{ ID: role.Client.ID.String(), Name: role.Client.Name, } } var menus []dto.RoleMenuResponse for _, m := range role.Menus { var parent *dto.RoleMenuResponse if m.Parent != nil { parent = &dto.RoleMenuResponse{ ID: m.Parent.ID.String(), Name: m.Parent.Name, IconUrl: m.Parent.IconUrl, Url: m.Parent.Url, Sequence: m.Parent.Sequence, Parent: nil, // atau rekursif jika ingin nested lebih dalam } } menus = append(menus, dto.RoleMenuResponse{ ID: m.ID.String(), Name: m.Name, IconUrl: m.IconUrl, Url: m.Url, Sequence: m.Sequence, Parent: parent, }) } return dto.RoleResponse{ ID: role.ID.String(), Name: role.Name, Description: role.Description, IconUrl: role.IconUrl, Type: role.Type, HomeUrl: role.HomeUrl, Level: role.Level, Client: client, Permissions: permissions, Menus: menus, } } func NewRoleService( roleRepo repository.RoleRepository, refreshTokenRepo authRepo.RefreshTokenRepository, jwtService service.JWTService, userService userService.UserService, db *gorm.DB, ) RoleService { return &roleService{ roleRepo: roleRepo, refreshTokenRepository: refreshTokenRepo, jwtService: jwtService, userService: userService, db: db, } }