From a1e9f601ce97869ab61b9ceba40d1a5bb0cfff96 Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Sun, 21 Jun 2026 23:41:10 +0100 Subject: [PATCH] feat(store): per-account inbound/outbound whitelist CRUD Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/store/whitelist.go | 88 ++++++++++++++++++++++++++++++++ internal/store/whitelist_test.go | 38 ++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 internal/store/whitelist.go create mode 100644 internal/store/whitelist_test.go diff --git a/internal/store/whitelist.go b/internal/store/whitelist.go new file mode 100644 index 0000000..f60a1d4 --- /dev/null +++ b/internal/store/whitelist.go @@ -0,0 +1,88 @@ +package store + +import ( + "fmt" + "strings" +) + +type Direction string + +const ( + DirIn Direction = "in" + DirOut Direction = "out" +) + +func (d Direction) table() (string, error) { + switch d { + case DirIn: + return "whitelist_in", nil + case DirOut: + return "whitelist_out", nil + default: + return "", fmt.Errorf("invalid direction %q", d) + } +} + +func (s *Store) accountID(name string) (int64, error) { + a, err := s.GetAccount(name) + if err != nil { + return 0, err + } + return a.ID, nil +} + +func (s *Store) AddWhitelist(account string, dir Direction, address string) error { + tbl, err := dir.table() + if err != nil { + return err + } + id, err := s.accountID(account) + if err != nil { + return err + } + _, err = s.db.Exec( + fmt.Sprintf("INSERT OR IGNORE INTO %s(account_id,address) VALUES(?,?)", tbl), + id, strings.ToLower(address)) + return err +} + +func (s *Store) RemoveWhitelist(account string, dir Direction, address string) error { + tbl, err := dir.table() + if err != nil { + return err + } + id, err := s.accountID(account) + if err != nil { + return err + } + _, err = s.db.Exec( + fmt.Sprintf("DELETE FROM %s WHERE account_id=? AND address=?", tbl), + id, strings.ToLower(address)) + return err +} + +func (s *Store) ListWhitelist(account string, dir Direction) ([]string, error) { + tbl, err := dir.table() + if err != nil { + return nil, err + } + id, err := s.accountID(account) + if err != nil { + return nil, err + } + rows, err := s.db.Query( + fmt.Sprintf("SELECT address FROM %s WHERE account_id=? ORDER BY address", tbl), id) + if err != nil { + return nil, err + } + defer rows.Close() + out := []string{} + for rows.Next() { + var a string + if err := rows.Scan(&a); err != nil { + return nil, err + } + out = append(out, a) + } + return out, rows.Err() +} diff --git a/internal/store/whitelist_test.go b/internal/store/whitelist_test.go new file mode 100644 index 0000000..3994524 --- /dev/null +++ b/internal/store/whitelist_test.go @@ -0,0 +1,38 @@ +package store + +import ( + "reflect" + "testing" +) + +func TestWhitelistAddListRemove(t *testing.T) { + s := openTemp(t) + _, _ = s.AddAccount(sampleAccount()) + + if err := s.AddWhitelist("work", DirIn, "Bob@Example.com"); err != nil { + t.Fatalf("add: %v", err) + } + _ = s.AddWhitelist("work", DirIn, "@trusted.com") + + got, err := s.ListWhitelist("work", DirIn) + if err != nil { + t.Fatalf("list: %v", err) + } + want := []string{"@trusted.com", "bob@example.com"} // lower-cased, sorted + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %v want %v", got, want) + } + + // Out list is independent. + if out, _ := s.ListWhitelist("work", DirOut); len(out) != 0 { + t.Fatalf("out list should be empty, got %v", out) + } + + if err := s.RemoveWhitelist("work", DirIn, "bob@example.com"); err != nil { + t.Fatalf("remove: %v", err) + } + got, _ = s.ListWhitelist("work", DirIn) + if !reflect.DeepEqual(got, []string{"@trusted.com"}) { + t.Fatalf("after remove got %v", got) + } +}