# Store Owner Audit — 2026-05-14 19:01:10 UTC

Running notes from the Store Owner accessible-pages audit. Bugs we
have already fixed are listed first for traceability; the open items
below are not yet patched and need a follow-up decision before merge.

## Fixed (commit b4defbe + this batch)

### Erratic table names (Hypervel surfaces these as empty 400s)
- `app/Http/Controllers/Market/ProductController.php:766`
  `pluck('stores.hashkey')` → `pluck('str.hashkey')`. Drove the
  Manage Listings modal in `ManageProductsAdmin.vue` to fail on every
  open.
- `app/Http/Controllers/Market/BatchController.php:91`
  `where('products.id', …)` → `where('prd_items.id', …)`. Broke the
  "already attached to this store" check in batch-import (existing
  mode).

### Validation/permission walls
- `ProductController::editProductAdmin` required `ModifyAllProducts`
  (Big-3 only) even when `data.store_hash` was supplied. STORE_OWNER
  edits via `UpdateProductModal.vue` returned 401. Now per-store
  edits only need `ModifyOwnProduct` / `AddProducttoOwnStore` plus
  the existing ownership branch.
- `ProductController::toggleProductStatus_Admin` — same gate, same
  relaxation.
- `ProductController::deleteProduct_Admin` required
  `DeleteAllProducts` (Big-3 only). Now accepts `DeleteOwnProduct`
  and the ancestor-of-creator path so owners can delete products
  they created.
- `PosController::voidSession` had no authorization check at all.
  Any session_hash could void any session. Now gated by
  `UserPermissions::isUserAllowedAccessToStore`.
- `PosAccessKeyController::destroy` and `toggleStatus` only checked
  the action permission. Now also verify the key belongs to a store
  the caller owns/manages.
- `StoreController::autoCreate` had no RBAC. Now requires
  Big-3 or STORE_OWNER, and dedupes generated names against the
  globally unique `str.name` index by appending a short suffix.
- `HomeStoreOwner.vue` referenced an undefined `creatingStore` ref
  (the actual ref is `creatingQuickStore`) — the "Creating your
  store..." spinner never rendered.

### BatchAddProducts opened to STORE_OWNER with guardrails
- `BatchController::batchCreateProducts` now allows STORE_OWNER, but
  only when `target_store_hash` is provided AND the target store is
  owned (or managed) by the caller. Without it the call is rejected
  up-front instead of silently 401-ing partway through the loop.
- New-mode rows are rejected when a global product with the same
  name (case-insensitive, trimmed) already exists. Owners are
  expected to pick the existing one via the fuzzy-search modal.
- `BatchAddProducts.vue` now redirects STORE_OWNER to CreateStore via
  a yes/no modal when they have zero selectable stores, and refuses
  submit if no target store is picked.

## Open — not yet patched, decide before merging

### `editStoreDetails` data leak (medium)
- File: `app/Http/Controllers/Market/StoreController.php:455`
- Route: `POST /Edit/Store/Details/data`
  (`routes/web.php:468-474`, middleware `auth + module:stores`).
- Behavior: looks up a `str` row by `hashkey`, and returns
  `name`, `category`, `subcategory`, `description`, `address`,
  `remarks`, `status`, `is_active`, `photourl`,
  `owner.hashkey`, `manager.hashkey`, all `store_managers` user
  hashkeys, all linked cooperative hashkeys, the parent-user
  selector list, and the dropzone payload — **without** any
  authorization check against the caller. Any authenticated user
  can issue this POST with any store's hashkey and get back the
  owner/manager/coop graph plus internal remarks. There is no
  modification path here, so this is information disclosure rather
  than escalation, but `remarks` is explicitly marked internal in
  the CreateStore flow and the manager/cooperative hashkeys leak
  the org graph.
- Suggested fix: require Big-3 OR
  (`store->owner_id`/`manager_id` matches caller)
  OR caller appears in `store_managers` pivot
  OR caller is ancestor of any of the above
  (the same predicate used by `canUserAccessPos` /
  `isUserAllowedAccessToStore`). Strip `remarks` from the response
  for non-Big-3.
- Status: **NOT FIXED IN THIS COMMIT.** Awaiting decision on
  whether to (a) tighten the endpoint and accept the chance of
  breaking any external/legacy caller, or (b) keep the
  read-anybody behavior and only strip `remarks`.

### Other observations (not bugs strictly)
- `editProductAdminByStore` (`ProductController.php:494`) has a
  `TODO Check first if store_id is owned by current user` comment
  and modifies the global product directly. Currently nothing
  routes to it, but leaving it in the codebase is a footgun if
  someone wires it up later. Recommend deleting.
- `listProducts_Admin` scopes non-Big-3 by `created_by`, so a
  store owner who attached a Big-3-created global product into
  their store cannot see/manage that product from "My Products".
  Per dictionary L265 this is intentional, but it does mean
  store-listings management for owners is split between
  "My Products" (creator-scoped) and the per-store flows
  (`AddProductsToStore`, `ManageStoresAdmin`).
- The Store Owner home's "Import Products" tile (`pagename:
  BatchAddProducts`) now works for STORE_OWNER thanks to the
  BatchController relaxation above. Pre-fix this would have
  loaded the page and then 401-ed on submit.

## Stores accessed during this audit
- `StoreController` (full file)
- `ProductController` (full file)
- `BatchController` (lines 33–172)
- `PosController` (sample around startSession/voidSession/getTodayStats)
- `PosAccessKeyController` (full file)
- Pages: `HomeStoreOwner.vue`, `CreateProductStoreOwner.vue`,
  `CreateStore.vue`, `AddProductsToStore.vue`, `BatchAddProducts.vue`,
  `ManageProductAdmin.vue`, `ManageProductsAdmin.vue`,
  `ManageStoresAdmin.vue`, `PosAccessKeys.vue`,
  `UpdateProductModal.vue`
- Routes: `routes/web.php` (home-data + Store/Product/POS blocks)
