package db import ( "context" "encoding/json" "github.com/pkg/errors" "github.com/uptrace/bun" ) type AuditLog struct { bun.BaseModel `bun:"table:audit_log,alias:al"` ID int `bun:"id,pk,autoincrement"` UserID int `bun:"user_id,notnull"` Action string `bun:"action,notnull"` ResourceType string `bun:"resource_type,notnull"` ResourceID *string `bun:"resource_id"` Details json.RawMessage `bun:"details,type:jsonb"` IPAddress string `bun:"ip_address"` UserAgent string `bun:"user_agent"` Result string `bun:"result,notnull"` // success, denied, error ErrorMessage *string `bun:"error_message"` CreatedAt int64 `bun:"created_at,notnull"` // Relations User *User `bun:"rel:belongs-to,join:user_id=id"` } // CreateAuditLog creates a new audit log entry func CreateAuditLog(ctx context.Context, tx bun.Tx, log *AuditLog) error { if log == nil { return errors.New("log cannot be nil") } err := Insert(tx, log).Exec(ctx) if err != nil { return errors.Wrap(err, "db.Insert") } return nil } type AuditLogFilter struct { *ListFilter } func NewAuditLogFilter() *AuditLogFilter { return &AuditLogFilter{ ListFilter: NewListFilter(), } } func (a *AuditLogFilter) UserID(id int) *AuditLogFilter { a.Equals("al.user_id", id) return a } func (a *AuditLogFilter) Action(action string) *AuditLogFilter { a.Equals("al.action", action) return a } func (a *AuditLogFilter) ResourceType(resourceType string) *AuditLogFilter { a.Equals("al.resource_type", resourceType) return a } func (a *AuditLogFilter) Result(result string) *AuditLogFilter { a.Equals("al.result", result) return a } func (a *AuditLogFilter) UserIDs(ids []int) *AuditLogFilter { if len(ids) > 0 { a.In("al.user_id", ids) } return a } func (a *AuditLogFilter) Actions(actions []string) *AuditLogFilter { if len(actions) > 0 { a.In("al.action", actions) } return a } func (a *AuditLogFilter) ResourceTypes(resourceTypes []string) *AuditLogFilter { if len(resourceTypes) > 0 { a.In("al.resource_type", resourceTypes) } return a } func (a *AuditLogFilter) Results(results []string) *AuditLogFilter { if len(results) > 0 { a.In("al.result", results) } return a } func (a *AuditLogFilter) DateRange(start, end int64) *AuditLogFilter { if start > 0 { a.GreaterEqualThan("al.created_at", start) } if end > 0 { a.LessEqualThan("al.created_at", end) } return a } // GetAuditLogs retrieves audit logs with optional filters and pagination func GetAuditLogs(ctx context.Context, tx bun.Tx, pageOpts *PageOpts, filters *AuditLogFilter) (*List[AuditLog], error) { defaultPageOpts := &PageOpts{ Page: 1, PerPage: 10, Order: bun.OrderDesc, OrderBy: "created_at", } return GetList[AuditLog](tx). Relation("User"). Filter(filters.filters...). GetPaged(ctx, pageOpts, defaultPageOpts) } // GetAuditLogsByUser retrieves audit logs for a specific user func GetAuditLogsByUser(ctx context.Context, tx bun.Tx, userID int, pageOpts *PageOpts) (*List[AuditLog], error) { if userID <= 0 { return nil, errors.New("userID must be positive") } filters := NewAuditLogFilter().UserID(userID) return GetAuditLogs(ctx, tx, pageOpts, filters) } // GetAuditLogsByAction retrieves audit logs for a specific action func GetAuditLogsByAction(ctx context.Context, tx bun.Tx, action string, pageOpts *PageOpts) (*List[AuditLog], error) { if action == "" { return nil, errors.New("action cannot be empty") } filters := NewAuditLogFilter().Action(action) return GetAuditLogs(ctx, tx, pageOpts, filters) } // GetAuditLogByID retrieves a single audit log by ID func GetAuditLogByID(ctx context.Context, tx bun.Tx, id int) (*AuditLog, error) { if id <= 0 { return nil, errors.New("id must be positive") } return GetByField[AuditLog](tx, "al.id", id).Relation("User").Get(ctx) } // GetUniqueActions retrieves a list of all unique actions in the audit log func GetUniqueActions(ctx context.Context, tx bun.Tx) ([]string, error) { var actions []string err := tx.NewSelect(). Model((*AuditLog)(nil)). Column("action"). Distinct(). Order("action ASC"). Scan(ctx, &actions) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect") } return actions, nil } // GetUniqueResourceTypes retrieves a list of all unique resource types in the audit log func GetUniqueResourceTypes(ctx context.Context, tx bun.Tx) ([]string, error) { var resourceTypes []string err := tx.NewSelect(). Model((*AuditLog)(nil)). Column("resource_type"). Distinct(). Order("resource_type ASC"). Scan(ctx, &resourceTypes) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect") } return resourceTypes, nil } // CleanupOldAuditLogs deletes audit logs older than the specified timestamp func CleanupOldAuditLogs(ctx context.Context, tx bun.Tx, olderThan int64) (int, error) { result, err := tx.NewDelete(). Model((*AuditLog)(nil)). Where("created_at < ?", olderThan). Exec(ctx) if err != nil { return 0, errors.Wrap(err, "tx.NewDelete") } rowsAffected, err := result.RowsAffected() if err != nil { return 0, errors.Wrap(err, "result.RowsAffected") } return int(rowsAffected), nil }