package usecase import ( "context" "regexp" "strings" "gateway/internal/model/permission/domain" "gateway/internal/model/permission/domain/entity" "gateway/internal/model/permission/domain/enum" domrepo "gateway/internal/model/permission/domain/repository" dom "gateway/internal/model/permission/domain/usecase" ) // roleKeyPattern enforces lower-case alphanumeric / underscore / dot keys. var roleKeyPattern = regexp.MustCompile(`^[a-z][a-z0-9._-]{1,63}$`) // RoleUseCaseParam injects the role + role-permission + user-role repos. // User roles are needed so Delete can refuse when assignments still exist. type RoleUseCaseParam struct { Roles domrepo.RoleRepository RolePermissions domrepo.RolePermissionRepository UserRoles domrepo.UserRoleRepository } type roleUseCase struct { roles domrepo.RoleRepository rolePerms domrepo.RolePermissionRepository userRoles domrepo.UserRoleRepository } // NewRoleUseCase returns the tenant-scoped role manager. func NewRoleUseCase(param RoleUseCaseParam) dom.RoleUseCase { return &roleUseCase{ roles: param.Roles, rolePerms: param.RolePermissions, userRoles: param.UserRoles, } } func (uc *roleUseCase) Create(ctx context.Context, param *dom.CreateRoleParam) (*entity.Role, error) { if param == nil || param.TenantID == "" { return nil, errb.InputMissingRequired("tenant_id") } if err := validateRoleKey(param.Key); err != nil { return nil, err } displayName := strings.TrimSpace(param.DisplayName) if displayName == "" { displayName = param.Key } if len(displayName) > domain.RoleDisplayNameMax { return nil, errb.InputInvalidRange("display_name too long") } status := param.Status if status == "" { status = enum.StatusOpen } if !status.IsValid() { return nil, errb.InputInvalidFormat("invalid status") } role := &entity.Role{ TenantID: param.TenantID, Key: param.Key, DisplayName: displayName, CreatorUID: param.CreatorUID, Status: status, IsSystem: false, } if err := uc.roles.Insert(ctx, role); err != nil { return nil, wrapRepoErr(err, "create role") } return role, nil } func (uc *roleUseCase) Get(ctx context.Context, tenantID, id string) (*entity.Role, error) { role, err := uc.roles.GetByID(ctx, tenantID, id) if err != nil { return nil, wrapRepoErr(err) } return role, nil } func (uc *roleUseCase) GetByKey(ctx context.Context, tenantID, key string) (*entity.Role, error) { role, err := uc.roles.GetByKey(ctx, tenantID, key) if err != nil { return nil, wrapRepoErr(err) } return role, nil } func (uc *roleUseCase) List(ctx context.Context, tenantID string) ([]*entity.Role, error) { roles, err := uc.roles.ListByTenant(ctx, tenantID) if err != nil { return nil, wrapRepoErr(err) } return roles, nil } func (uc *roleUseCase) Update( ctx context.Context, tenantID, id string, param *dom.UpdateRoleParam, ) (*entity.Role, error) { if param == nil { return uc.Get(ctx, tenantID, id) } existing, err := uc.roles.GetByID(ctx, tenantID, id) if err != nil { return nil, wrapRepoErr(err) } if existing.IsSystem && param.Status != nil { return nil, wrapRepoErr(domain.ErrRoleSystemImmutable) } update := &domrepo.RoleUpdate{} if param.DisplayName != nil { display := strings.TrimSpace(*param.DisplayName) if display == "" { return nil, errb.InputMissingRequired("display_name") } if len(display) > domain.RoleDisplayNameMax { return nil, errb.InputInvalidRange("display_name too long") } update.DisplayName = &display } if param.Status != nil { if !param.Status.IsValid() { return nil, errb.InputInvalidFormat("invalid status") } s := param.Status.String() update.Status = &s } role, err := uc.roles.Update(ctx, tenantID, id, update) if err != nil { return nil, wrapRepoErr(err, "update role") } return role, nil } func (uc *roleUseCase) Delete(ctx context.Context, tenantID, id string) error { existing, err := uc.roles.GetByID(ctx, tenantID, id) if err != nil { return wrapRepoErr(err) } if existing.IsSystem { return wrapRepoErr(domain.ErrRoleSystemImmutable) } assignments, err := uc.userRoles.ListByRole(ctx, tenantID, id) if err != nil { return wrapRepoErr(err, "check user roles") } if len(assignments) > 0 { return errb.ResPreconditionFailed("role still has assignments") } if err := uc.rolePerms.DeleteByRole(ctx, tenantID, id); err != nil { return wrapRepoErr(err, "delete role permissions") } if err := uc.roles.Delete(ctx, tenantID, id); err != nil { return wrapRepoErr(err, "delete role") } return nil } func validateRoleKey(key string) error { key = strings.TrimSpace(key) if key == "" { return errb.InputMissingRequired("key") } if len(key) < domain.RoleKeyMinLength || len(key) > domain.RoleKeyMaxLength { return errb.InputInvalidRange("key length") } if !roleKeyPattern.MatchString(key) { return wrapRepoErr(domain.ErrRoleKeyInvalid) } for _, prefix := range domain.ReservedRoleKeyPrefixes { if strings.HasPrefix(key, prefix) { return wrapRepoErr(domain.ErrRoleKeyReserved) } } return nil } var _ dom.RoleUseCase = (*roleUseCase)(nil)