# 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)