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.
142 lines
4.4 KiB
Go
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
|
|
}
|