package audit import ( "context" "database/sql" "encoding/json" "fmt" "sync" "time" ) type Entry struct { ID int64 `json:"id"` TenantID int64 `json:"tenant_id"` UserID int64 `json:"user_id"` Action string `json:"action"` Detail string `json:"detail"` CreatedAt time.Time `json:"created_at"` } type Store interface { Log(ctx context.Context, tenantID, userID int64, action, detail string) error List(ctx context.Context, tenantID int64, limit, offset int) ([]Entry, int, error) } type MemoryStore struct { mu sync.Mutex seq int64 rows []Entry } func NewMemoryStore() *MemoryStore { return &MemoryStore{} } func (s *MemoryStore) Log(_ context.Context, tenantID, userID int64, action, detail string) error { s.mu.Lock() defer s.mu.Unlock() s.seq++ s.rows = append(s.rows, Entry{ ID: s.seq, TenantID: tenantID, UserID: userID, Action: action, Detail: detail, CreatedAt: time.Now().UTC(), }) return nil } func (s *MemoryStore) List(_ context.Context, tenantID int64, limit, offset int) ([]Entry, int, error) { s.mu.Lock() defer s.mu.Unlock() if limit <= 0 { limit = 50 } filtered := make([]Entry, 0) for i := len(s.rows) - 1; i >= 0; i-- { if s.rows[i].TenantID == tenantID { filtered = append(filtered, s.rows[i]) } } total := len(filtered) if offset >= total { return []Entry{}, total, nil } end := offset + limit if end > total { end = total } return filtered[offset:end], total, nil } type PostgresStore struct { DB *sql.DB } func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db} } func (s *PostgresStore) Log(ctx context.Context, tenantID, userID int64, action, detail string) error { _, err := s.DB.ExecContext(ctx, ` INSERT INTO platform_meta.audit_logs(tenant_id, user_id, action, detail) VALUES($1,$2,$3,$4)`, tenantID, userID, action, detail) return err } func (s *PostgresStore) List(ctx context.Context, tenantID int64, limit, offset int) ([]Entry, int, error) { if limit <= 0 { limit = 50 } var total int if err := s.DB.QueryRowContext(ctx, `SELECT COUNT(1) FROM platform_meta.audit_logs WHERE tenant_id=$1`, tenantID, ).Scan(&total); err != nil { return nil, 0, err } rows, err := s.DB.QueryContext(ctx, ` SELECT id, tenant_id, user_id, action, detail, created_at FROM platform_meta.audit_logs WHERE tenant_id=$1 ORDER BY id DESC LIMIT $2 OFFSET $3`, tenantID, limit, offset) if err != nil { return nil, 0, err } defer rows.Close() out := make([]Entry, 0) for rows.Next() { var e Entry if err := rows.Scan(&e.ID, &e.TenantID, &e.UserID, &e.Action, &e.Detail, &e.CreatedAt); err != nil { return nil, 0, err } out = append(out, e) } return out, total, rows.Err() } func DetailJSON(v any) string { b, err := json.Marshal(v) if err != nil { return fmt.Sprint(v) } return string(b) }