140 lines
4.2 KiB
Go
140 lines
4.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// AuditService implements business logic for AuditLog operations.
|
|
// Reference: Chatwoot enterprise audit log feature + P2B M11 spec
|
|
type AuditService struct {
|
|
repo *repository.AuditRepo
|
|
}
|
|
|
|
// AuditRecord captures the request/user metadata needed to create a Chatwoot-style audit row.
|
|
type AuditRecord struct {
|
|
AccountID uint
|
|
UserID uint
|
|
Username string
|
|
AuditableType string
|
|
AuditableID uint
|
|
Action string
|
|
AuditedChanges interface{}
|
|
RemoteAddress string
|
|
RequestUUID string
|
|
Comment string
|
|
}
|
|
|
|
// NewAuditService creates a new AuditLog service.
|
|
func NewAuditService(repo *repository.AuditRepo) *AuditService {
|
|
return &AuditService{repo: repo}
|
|
}
|
|
|
|
// ListByAccount retrieves paginated audit log entries for an account with optional filters.
|
|
// page and pageSize follow the project pagination convention (page >= 1, pageSize capped at 100).
|
|
func (s *AuditService) ListByAccount(ctx context.Context, accountID uint, action string, auditableType string, page int, pageSize int) ([]model.Audit, int64, error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 25
|
|
}
|
|
if pageSize > 100 {
|
|
pageSize = 100
|
|
}
|
|
|
|
offset := (page - 1) * pageSize
|
|
audits, total, err := s.repo.FindByAccount(ctx, accountID, action, auditableType, offset, pageSize)
|
|
if err != nil {
|
|
applogger.L().Errorf("AuditService.ListByAccount account=%d action=%s auditableType=%s: %v", accountID, action, auditableType, err)
|
|
return nil, 0, fmt.Errorf("failed to list audit logs: %w", err)
|
|
}
|
|
|
|
return audits, total, nil
|
|
}
|
|
|
|
// CreateAudit creates a new audit log entry.
|
|
func (s *AuditService) CreateAudit(ctx context.Context, audit *model.Audit) (*model.Audit, error) {
|
|
if err := s.repo.Create(ctx, audit); err != nil {
|
|
applogger.L().Errorf("AuditService.CreateAudit: %v", err)
|
|
return nil, fmt.Errorf("failed to create audit log: %w", err)
|
|
}
|
|
return audit, nil
|
|
}
|
|
|
|
// Record creates an account-associated audit row for a mutating account resource.
|
|
func (s *AuditService) Record(ctx context.Context, record AuditRecord) (*model.Audit, error) {
|
|
if record.AccountID == 0 || record.AuditableType == "" || record.AuditableID == 0 || record.Action == "" {
|
|
return nil, fmt.Errorf("invalid audit record")
|
|
}
|
|
|
|
changes, err := marshalAuditChanges(record.AuditedChanges)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid audited changes: %w", err)
|
|
}
|
|
|
|
accountID := record.AccountID
|
|
audit := &model.Audit{
|
|
AccountID: &accountID,
|
|
AuditableType: record.AuditableType,
|
|
AuditableID: record.AuditableID,
|
|
Action: record.Action,
|
|
AuditedChanges: changes,
|
|
AssociatedType: "Account",
|
|
AssociatedID: &accountID,
|
|
Username: record.Username,
|
|
RemoteAddress: record.RemoteAddress,
|
|
RequestUUID: record.RequestUUID,
|
|
Comment: record.Comment,
|
|
}
|
|
if record.UserID != 0 {
|
|
userID := record.UserID
|
|
audit.UserID = &userID
|
|
audit.UserType = "User"
|
|
}
|
|
|
|
return s.CreateAudit(ctx, audit)
|
|
}
|
|
|
|
// GetByID retrieves a single audit log entry by ID.
|
|
func (s *AuditService) GetByID(ctx context.Context, id uint) (*model.Audit, error) {
|
|
audit, err := s.repo.FindByID(ctx, id)
|
|
if err != nil {
|
|
applogger.L().Errorf("AuditService.GetByID id=%d: %v", id, err)
|
|
return nil, fmt.Errorf("audit log not found: %w", err)
|
|
}
|
|
return audit, nil
|
|
}
|
|
|
|
// GetByIDForAccount retrieves a single audit log entry scoped to an account.
|
|
func (s *AuditService) GetByIDForAccount(ctx context.Context, accountID uint, id uint) (*model.Audit, error) {
|
|
audit, err := s.repo.FindByIDForAccount(ctx, accountID, id)
|
|
if err != nil {
|
|
applogger.L().Errorf("AuditService.GetByIDForAccount account=%d id=%d: %v", accountID, id, err)
|
|
return nil, fmt.Errorf("audit log not found: %w", err)
|
|
}
|
|
return audit, nil
|
|
}
|
|
|
|
func marshalAuditChanges(changes interface{}) (json.RawMessage, error) {
|
|
if changes == nil {
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
if raw, ok := changes.(json.RawMessage); ok {
|
|
if len(raw) == 0 {
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
return raw, nil
|
|
}
|
|
data, err := json.Marshal(changes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.RawMessage(data), nil
|
|
}
|