Files
gochat/backend/internal/service/custom_role_service.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

142 lines
4.4 KiB
Go

package service
import (
"context"
"fmt"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
applogger "github.com/gochat/gochat/pkg/logger"
)
// CustomRoleService implements business logic for CustomRole operations.
// Reference: Chatwoot enterprise/app/models/custom_role.rb
type CustomRoleService struct {
repo *repository.CustomRoleRepo
}
// NewCustomRoleService creates a new CustomRole service.
func NewCustomRoleService(repo *repository.CustomRoleRepo) *CustomRoleService {
return &CustomRoleService{repo: repo}
}
// CreateCustomRoleRequest is the DTO for creating a custom role.
type CreateCustomRoleRequest struct {
Name string `json:"name" validate:"required,min=2"`
Permissions []string `json:"permissions"`
Description string `json:"description,omitempty"`
}
// UpdateCustomRoleRequest is the DTO for updating a custom role.
type UpdateCustomRoleRequest struct {
Name string `json:"name,omitempty" validate:"omitempty,min=2"`
Permissions []string `json:"permissions,omitempty"`
Description *string `json:"description,omitempty"`
}
// List retrieves all custom roles for an account with pagination.
func (s *CustomRoleService) List(ctx context.Context, accountID uint, page, pageSize int) ([]model.CustomRole, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 25
}
offset := (page - 1) * pageSize
roles, total, err := s.repo.FindByAccount(ctx, accountID, offset, pageSize)
if err != nil {
applogger.L().Errorf("List custom roles for account %d: %v", accountID, err)
return nil, 0, fmt.Errorf("failed to list custom roles: %w", err)
}
return roles, total, nil
}
// Create creates a new custom role within an account.
func (s *CustomRoleService) Create(ctx context.Context, accountID uint, req CreateCustomRoleRequest) (*model.CustomRole, error) {
role := &model.CustomRole{
AccountID: accountID,
Name: req.Name,
Description: req.Description,
}
if req.Permissions != nil {
if err := role.SetPermissionKeys(permissionStringsToDimensions(req.Permissions)); err != nil {
return nil, fmt.Errorf("invalid permissions: %w", err)
}
} else {
role.Permissions = "[]"
}
// Validate the role before persisting
if err := role.Validate(); err != nil {
return nil, err
}
if err := s.repo.Create(ctx, role); err != nil {
applogger.L().Errorf("failed to create custom role: %v", err)
return nil, fmt.Errorf("failed to create custom role: %w", err)
}
return role, nil
}
// Update updates an existing custom role scoped to an account.
func (s *CustomRoleService) Update(ctx context.Context, id, accountID uint, req UpdateCustomRoleRequest) (*model.CustomRole, error) {
role, err := s.repo.FindByIDAndAccount(ctx, id, accountID)
if err != nil {
return nil, fmt.Errorf("custom role not found: %w", err)
}
// Merge fields: only update non-empty values
if req.Name != "" {
role.Name = req.Name
}
if req.Description != nil {
role.Description = *req.Description
}
// Merge permissions: if provided, replace entirely
if req.Permissions != nil {
if err := role.SetPermissionKeys(permissionStringsToDimensions(req.Permissions)); err != nil {
return nil, fmt.Errorf("invalid permissions: %w", err)
}
}
// Validate the updated role before persisting
if err := role.Validate(); err != nil {
return nil, err
}
if err := s.repo.Update(ctx, role); err != nil {
applogger.L().Errorf("failed to update custom role: %v", err)
return nil, fmt.Errorf("failed to update custom role: %w", err)
}
return role, nil
}
func permissionStringsToDimensions(keys []string) []model.PermissionDimension {
dims := make([]model.PermissionDimension, 0, len(keys))
for _, key := range keys {
dims = append(dims, model.PermissionDimension(key))
}
return dims
}
// Delete soft-deletes a custom role scoped to an account.
func (s *CustomRoleService) Delete(ctx context.Context, id, accountID uint) error {
if err := s.repo.Delete(ctx, id, accountID); err != nil {
applogger.L().Errorf("failed to delete custom role %d for account %d: %v", id, accountID, err)
return fmt.Errorf("failed to delete custom role: %w", err)
}
return nil
}
// GetByID retrieves a single custom role scoped to an account.
func (s *CustomRoleService) GetByID(ctx context.Context, id, accountID uint) (*model.CustomRole, error) {
role, err := s.repo.FindByIDAndAccount(ctx, id, accountID)
if err != nil {
return nil, fmt.Errorf("custom role not found: %w", err)
}
return role, nil
}