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" "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" "github.com/Caknoooo/go-gin-clean-starter/pkg/utils" "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 log *logrus.Logger } // 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, dto.ToRoleResponse(role)) } return responses, nil } // 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) { userID := "" if ctx != nil { userID = utils.GetUserID(ctx) } 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, Level: req.Level, Type: req.Type, HomeUrl: req.HomeUrl, ClientID: clientUUID, FullAuditTrail: utils.FillAuditTrail(ctx, constants.CREATE), } 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 } r.log.WithFields(logrus.Fields{ "user_id": userID, "action": "create", "entity": "role", "entity_id": createdRole.ID.String(), }).Info("Role created") return dto.ToRoleResponse(result), nil } // DeleteRole implements RoleService. func (r *roleService) DeleteRole(ctx context.Context, id string) error { role, err := r.roleRepo.GetRoleByID(ctx, r.db, id) if err != nil { return dto.ErrRoleNotFound } role.FullAuditTrail = utils.FillAuditTrail(ctx, constants.DELETE) if _, err := r.roleRepo.UpdateRole(ctx, r.db, role); err != nil { return err } if err := r.roleRepo.DeleteRole(ctx, r.db, id); err != nil { return err } userID := "" if ctx != nil { userID = utils.GetUserID(ctx) } r.log.WithFields(logrus.Fields{ "user_id": userID, "action": "delete", "entity": "role", "entity_id": id, }).Info("Role deleted") return nil } // 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 dto.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 { return nil, err } var responses []dto.RoleResponse for _, role := range roles { responses = append(responses, dto.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 } before := existingRole if req.Name != "" { 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 } if req.Level != 0 { existingRole.Level = req.Level } existingRole.FullAuditTrail = utils.FillAuditTrail(ctx, constants.UPDATE) 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 } changes := utils.GetChangedFields(before, result) userID := "" if ctx != nil { userID = utils.GetUserID(ctx) } r.log.WithFields(logrus.Fields{ "user_id": userID, "action": "update", "entity": "role", "entity_id": id, "changes": changes, }).Info("Role updated") return dto.ToRoleResponse(result), nil } func NewRoleService( roleRepo repository.RoleRepository, refreshTokenRepo authRepo.RefreshTokenRepository, jwtService service.JWTService, userService userService.UserService, db *gorm.DB, log *logrus.Logger, ) RoleService { return &roleService{ roleRepo: roleRepo, refreshTokenRepository: refreshTokenRepo, jwtService: jwtService, userService: userService, db: db, log: log, } }