Add per-user item ownership and isolation

Items are now private to their owner. Add items.user_id (migrated in
place, existing items backfilled to the first admin) and scope every
item, results, and dashboard view to the signed-in user via owner-scoped
queries plus an ownedItem 404 guard. Admins get strict isolation too;
elevation stays limited to settings and user management.

Alert routing follows ownership: deal emails go only to the item owner
and the weekly digest is built per-recipient from their own items. The
scheduler still polls every active item; the Apify/eBay budget stays a
shared pool visible to all.

Add TestItemsArePrivatePerUser and seed owners in db tests.
This commit is contained in:
prosolis
2026-06-20 13:39:19 -07:00
parent 90a74967e0
commit feea126c5e
11 changed files with 327 additions and 72 deletions

View File

@@ -23,15 +23,27 @@ func newTestStore(t *testing.T) *Store {
return NewStore(conn, key)
}
// seedUser creates an owner so item rows satisfy the items.user_id foreign key.
func seedUser(t *testing.T, s *Store) int64 {
t.Helper()
id, err := s.CreateUser(context.Background(), "tester", "!x", models.RoleAdmin)
if err != nil {
t.Fatal(err)
}
return id
}
func TestDedupByItemAndURL(t *testing.T) {
if os.Getenv("CI_SKIP_SQLITE") != "" {
t.Skip()
}
s := newTestStore(t)
ctx := context.Background()
uid := seedUser(t, s)
id, err := s.CreateItem(ctx, &models.Item{
Name: "TwinBee", NtfyTopic: "veola", Active: true,
UserID: uid,
Name: "TwinBee", NtfyTopic: "veola", Active: true,
PollIntervalMinutes: 60, NtfyPriority: "default",
})
if err != nil {
@@ -62,7 +74,7 @@ func TestDedupByItemAndURL(t *testing.T) {
}
// Different item, same URL should not collide.
id2, _ := s.CreateItem(ctx, &models.Item{Name: "Other", NtfyTopic: "veola", PollIntervalMinutes: 60, NtfyPriority: "default"})
id2, _ := s.CreateItem(ctx, &models.Item{UserID: uid, Name: "Other", NtfyTopic: "veola", PollIntervalMinutes: 60, NtfyPriority: "default"})
other, _ := s.ResultExists(ctx, id2, "https://example.com/listing/1")
if other {
t.Error("dedup should be scoped to item_id")
@@ -81,10 +93,12 @@ func TestDashboardMoneySavedWindow(t *testing.T) {
}
s := newTestStore(t)
ctx := context.Background()
uid := seedUser(t, s)
bp := 100.0
id, err := s.CreateItem(ctx, &models.Item{
Name: "Windowed", NtfyTopic: "veola", Active: true,
UserID: uid,
Name: "Windowed", NtfyTopic: "veola", Active: true,
PollIntervalMinutes: 60, NtfyPriority: "default",
BestPrice: &bp,
})
@@ -123,7 +137,9 @@ func TestDashboardMoneySavedWindow(t *testing.T) {
func TestCJKRoundTrip(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
uid := seedUser(t, s)
id, err := s.CreateItem(ctx, &models.Item{
UserID: uid,
Name: "ツインビー",
SearchQuery: "ツインビー グラディウス パロディウス",
NtfyTopic: "veola",