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:
@@ -20,9 +20,23 @@ import (
|
||||
// form: 12 hours, chosen to keep Apify spend low by default.
|
||||
const defaultPollIntervalMinutes = 720
|
||||
|
||||
// ownedItem fetches an item and confirms the current user owns it. It returns
|
||||
// nil when the item is missing or owned by someone else, so callers treat both
|
||||
// cases as a 404 and never leak another user's items.
|
||||
func (a *App) ownedItem(r *http.Request, id int64) (*models.Item, error) {
|
||||
it, err := a.Store.GetItem(r.Context(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if it == nil || it.UserID != a.userID(r) {
|
||||
return nil, nil
|
||||
}
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (a *App) GetItems(w http.ResponseWriter, r *http.Request) {
|
||||
cat := r.URL.Query().Get("category")
|
||||
all, err := a.Store.ListItems(r.Context())
|
||||
all, err := a.Store.ListItemsForUser(r.Context(), a.userID(r))
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -33,7 +47,7 @@ func (a *App) GetItems(w http.ResponseWriter, r *http.Request) {
|
||||
items = append(items, it)
|
||||
}
|
||||
}
|
||||
cats, _ := a.Store.ListCategories(r.Context())
|
||||
cats, _ := a.Store.ListCategoriesForUser(r.Context(), a.userID(r))
|
||||
|
||||
// Bulk-load recent price history so each row can render a sparkline
|
||||
// without N+1 queries. 20 points is enough for a meaningful trend line
|
||||
@@ -54,7 +68,7 @@ func (a *App) GetItems(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (a *App) GetNewItem(w http.ResponseWriter, r *http.Request) {
|
||||
cats, _ := a.Store.ListCategories(r.Context())
|
||||
cats, _ := a.Store.ListCategoriesForUser(r.Context(), a.userID(r))
|
||||
render(w, r, templates.ItemForm(templates.ItemFormData{
|
||||
Page: a.page(r, "Add Item", "items"),
|
||||
IsEdit: false,
|
||||
@@ -73,12 +87,12 @@ func (a *App) GetNewItem(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (a *App) GetEditItem(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
|
||||
}
|
||||
cats, _ := a.Store.ListCategories(r.Context())
|
||||
cats, _ := a.Store.ListCategoriesForUser(r.Context(), a.userID(r))
|
||||
render(w, r, templates.ItemForm(templates.ItemFormData{
|
||||
Page: a.page(r, "Edit "+it.Name, "items"),
|
||||
IsEdit: true,
|
||||
@@ -341,6 +355,7 @@ func (a *App) PostCreateItem(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, strings.Join(errs, "; "), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
it.UserID = a.userID(r)
|
||||
id, err := a.Store.CreateItem(r.Context(), &it)
|
||||
if err != nil {
|
||||
a.serverError(w, r, err)
|
||||
@@ -364,14 +379,14 @@ func (a *App) PostCreateItem(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (a *App) PostUpdateItem(w http.ResponseWriter, r *http.Request) {
|
||||
id := intParam(r, "id")
|
||||
existing, err := a.Store.GetItem(r.Context(), id)
|
||||
existing, err := a.ownedItem(r, id)
|
||||
if err != nil || existing == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
updated, errs := parseItemForm(r)
|
||||
if len(errs) > 0 {
|
||||
cats, _ := a.Store.ListCategories(r.Context())
|
||||
cats, _ := a.Store.ListCategoriesForUser(r.Context(), a.userID(r))
|
||||
updated.ID = id
|
||||
render(w, r, templates.ItemForm(templates.ItemFormData{
|
||||
Page: a.page(r, "Edit "+updated.Name, "items"),
|
||||
@@ -394,7 +409,7 @@ func (a *App) PostUpdateItem(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (a *App) PostToggleItem(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
|
||||
@@ -411,6 +426,11 @@ func (a *App) PostToggleItem(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (a *App) PostDeleteItem(w http.ResponseWriter, r *http.Request) {
|
||||
id := intParam(r, "id")
|
||||
it, err := a.ownedItem(r, id)
|
||||
if err != nil || it == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if err := a.Store.DeleteItem(r.Context(), id); err != nil {
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
@@ -421,7 +441,7 @@ func (a *App) PostDeleteItem(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (a *App) PostRunItem(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
|
||||
@@ -455,7 +475,7 @@ func (a *App) PostRunItem(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (a *App) GetItemError(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
|
||||
|
||||
Reference in New Issue
Block a user