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.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
package model
|
||||
|
||||
// CustomRole represents an enterprise custom role with Chatwoot-compatible
|
||||
// permission keys. Chatwoot stores permissions as a text array; GoChat stores the
|
||||
// same string array as JSON for portability while keeping legacy map reads.
|
||||
// Reference: Chatwoot enterprise/app/models/custom_role.rb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PermissionDimension constants are duplicated here to avoid a circular import
|
||||
// with auth. The first six constants are the current Chatwoot custom-role keys.
|
||||
type PermissionDimension string
|
||||
|
||||
const (
|
||||
DimensionConversationManage PermissionDimension = "conversation_manage"
|
||||
DimensionConversationUnassignedManage PermissionDimension = "conversation_unassigned_manage"
|
||||
DimensionConversationParticipatingManage PermissionDimension = "conversation_participating_manage"
|
||||
DimensionContactManage PermissionDimension = "contact_manage"
|
||||
DimensionReportManage PermissionDimension = "report_manage"
|
||||
DimensionKnowledgeBaseManage PermissionDimension = "knowledge_base_manage"
|
||||
|
||||
// Legacy local dimensions. These are accepted when reading older JSON map
|
||||
// values but are not valid Chatwoot custom-role API keys.
|
||||
DimensionConversationDelete PermissionDimension = "conversation_delete"
|
||||
DimensionAutomationManage PermissionDimension = "automation_manage"
|
||||
)
|
||||
|
||||
// PermissionLevel constants — duplicated here to avoid circular import with auth package.
|
||||
type PermissionLevel string
|
||||
|
||||
const (
|
||||
PermissionLevelFull PermissionLevel = "full"
|
||||
PermissionLevelRead PermissionLevel = "read"
|
||||
PermissionLevelNone PermissionLevel = "none"
|
||||
)
|
||||
|
||||
// CustomRole represents a custom enterprise role definition.
|
||||
type CustomRole struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AccountID uint `gorm:"not null;index" json:"account_id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Permissions string `gorm:"type:jsonb;not null;default:'[]'" json:"permissions"` // JSON array of Chatwoot permission keys.
|
||||
Description string `gorm:"size:500" json:"description,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
|
||||
|
||||
// Relations
|
||||
Account Account `gorm:"foreignKey:AccountID" json:"account,omitempty"`
|
||||
AccountUsers []AccountUser `gorm:"foreignKey:CustomRoleID" json:"account_users,omitempty"`
|
||||
}
|
||||
|
||||
func (CustomRole) TableName() string { return "custom_roles" }
|
||||
|
||||
var validCustomRolePermissions = map[PermissionDimension]struct{}{
|
||||
DimensionConversationManage: {},
|
||||
DimensionConversationUnassignedManage: {},
|
||||
DimensionConversationParticipatingManage: {},
|
||||
DimensionContactManage: {},
|
||||
DimensionReportManage: {},
|
||||
DimensionKnowledgeBaseManage: {},
|
||||
}
|
||||
|
||||
// IsValidCustomRolePermission reports whether key is one of Chatwoot's current
|
||||
// CustomRole::PERMISSIONS values.
|
||||
func IsValidCustomRolePermission(key PermissionDimension) bool {
|
||||
_, ok := validCustomRolePermissions[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetPermissionKeys returns Chatwoot custom-role permission keys. It accepts the
|
||||
// current JSON array shape and the legacy local JSON map shape for old rows.
|
||||
func (cr *CustomRole) GetPermissionKeys() ([]PermissionDimension, error) {
|
||||
if cr.Permissions == "" || cr.Permissions == "{}" || cr.Permissions == "[]" || cr.Permissions == "null" {
|
||||
return []PermissionDimension{}, nil
|
||||
}
|
||||
|
||||
var keys []PermissionDimension
|
||||
if err := json.Unmarshal([]byte(cr.Permissions), &keys); err == nil {
|
||||
return normalizePermissionKeys(keys)
|
||||
}
|
||||
|
||||
legacyMap, err := cr.GetPermissionMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys = make([]PermissionDimension, 0, len(legacyMap))
|
||||
for dim, level := range legacyMap {
|
||||
if !IsValidCustomRolePermission(dim) || level == PermissionLevelNone {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, dim)
|
||||
}
|
||||
return normalizePermissionKeys(keys)
|
||||
}
|
||||
|
||||
// SetPermissionKeys stores Chatwoot custom-role permission keys as a JSON array.
|
||||
func (cr *CustomRole) SetPermissionKeys(keys []PermissionDimension) error {
|
||||
normalized, err := normalizePermissionKeys(keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.Marshal(normalized)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize custom role permissions: %w", err)
|
||||
}
|
||||
cr.Permissions = string(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPermissionMap deserializes the JSONB permissions field into a map of
|
||||
// PermissionDimension → PermissionLevel. This is the raw deserialization;
|
||||
// the auth.PolicyContext layer converts this into auth.PermissionMatrix for
|
||||
// policy evaluation.
|
||||
func (cr *CustomRole) GetPermissionMap() (map[PermissionDimension]PermissionLevel, error) {
|
||||
var keys []PermissionDimension
|
||||
if err := json.Unmarshal([]byte(cr.Permissions), &keys); err == nil {
|
||||
pm := map[PermissionDimension]PermissionLevel{}
|
||||
normalized, err := normalizePermissionKeys(keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, key := range normalized {
|
||||
pm[key] = PermissionLevelFull
|
||||
}
|
||||
return pm, nil
|
||||
}
|
||||
|
||||
var m map[PermissionDimension]PermissionLevel
|
||||
if cr.Permissions == "" || cr.Permissions == "{}" {
|
||||
return map[PermissionDimension]PermissionLevel{}, nil
|
||||
}
|
||||
if err := json.Unmarshal([]byte(cr.Permissions), &m); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse custom role permissions JSON: %w", err)
|
||||
}
|
||||
for dim, level := range m {
|
||||
if level != PermissionLevelFull && level != PermissionLevelRead && level != PermissionLevelNone {
|
||||
return nil, fmt.Errorf("invalid permission level '%s' for dimension '%s'", level, dim)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// SetPermissionMap stores non-none valid Chatwoot permissions from a legacy
|
||||
// permission matrix as the current JSON array shape.
|
||||
func (cr *CustomRole) SetPermissionMap(m map[PermissionDimension]PermissionLevel) error {
|
||||
keys := make([]PermissionDimension, 0, len(m))
|
||||
for dim, level := range m {
|
||||
if level == PermissionLevelNone || !IsValidCustomRolePermission(dim) {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, dim)
|
||||
}
|
||||
return cr.SetPermissionKeys(keys)
|
||||
}
|
||||
|
||||
// Validate checks that the custom role has a valid name and permission dimensions.
|
||||
func (cr *CustomRole) Validate() error {
|
||||
if cr.Name == "" {
|
||||
return fmt.Errorf("custom role name is required")
|
||||
}
|
||||
|
||||
keys, err := cr.GetPermissionKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if !IsValidCustomRolePermission(key) {
|
||||
return fmt.Errorf("invalid custom role permission '%s'", key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizePermissionKeys(keys []PermissionDimension) ([]PermissionDimension, error) {
|
||||
seen := map[PermissionDimension]struct{}{}
|
||||
normalized := make([]PermissionDimension, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if !IsValidCustomRolePermission(key) {
|
||||
return nil, fmt.Errorf("invalid custom role permission '%s'", key)
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
normalized = append(normalized, key)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
// CustomRoleValidationError represents a validation error for a custom role.
|
||||
type CustomRoleValidationError struct {
|
||||
Dimension string
|
||||
Level string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *CustomRoleValidationError) Error() string {
|
||||
return "custom_role validation error: dimension '" + e.Dimension + "' has invalid level '" + e.Level + "' — " + e.Message
|
||||
}
|
||||
Reference in New Issue
Block a user