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

@@ -16,7 +16,7 @@ const resultsPerPage = 20
func (a *App) GetItemResults(w http.ResponseWriter, r *http.Request) {
id := intParam(r, "id")
it, err := a.Store.GetItem(r.Context(), id)
it, err := a.ownedItem(r, id)
if err != nil || it == nil {
http.NotFound(w, r)
return
@@ -71,7 +71,7 @@ func (a *App) buildItemResultsData(r *http.Request, it *models.Item, page int, o
// 24h surface for the "ending soon" strip — beyond that, a static
// "ends in 4 days" in the per-row cell carries enough signal on its own.
endingSoon, _ := a.Store.NextEndingResult(r.Context(), it.ID, 24*time.Hour)
endingSoon, _ := a.Store.NextEndingResult(r.Context(), it.ID, 0, 24*time.Hour)
return templates.ItemResultsData{
Page: a.page(r, it.Name, "items"),
@@ -105,7 +105,8 @@ func (a *App) GetGlobalResults(w http.ResponseWriter, r *http.Request) {
from := strings.TrimSpace(q.Get("from"))
to := strings.TrimSpace(q.Get("to"))
items, err := a.Store.ListItems(r.Context())
uid := a.userID(r)
items, err := a.Store.ListItemsForUser(r.Context(), uid)
if err != nil {
a.serverError(w, r, err)
return
@@ -117,6 +118,7 @@ func (a *App) GetGlobalResults(w http.ResponseWriter, r *http.Request) {
results, err := a.Store.ListResults(r.Context(), db.ResultsQuery{
ItemID: itemID,
OwnerID: uid,
Limit: 200,
ExcludeEnded: true,
})
@@ -145,7 +147,7 @@ func (a *App) GetGlobalResults(w http.ResponseWriter, r *http.Request) {
})
}
endingSoon, _ := a.Store.NextEndingResult(r.Context(), itemID, 24*time.Hour)
endingSoon, _ := a.Store.NextEndingResult(r.Context(), itemID, uid, 24*time.Hour)
render(w, r, templates.GlobalResults(templates.GlobalResultsData{
Page: a.page(r, "Results", "results"),