# Authorization & CRUD Audit — Continuation File Branch: `authz-audit-2026-08-25` (diverged from `main` in all four repos). Last checkpoint: 2026-08-26 — LIVE authorization verification executed (see "Session 2026-08-26c"). ## What is DONE (committed on this branch) ### Backend (`github.com/TaruLab/keluargamart-backend`) - Module path renamed repo-wide: `b-keluargamart` → `keluargamart-backend`. - Every `/api/v1` group now has an application / permission gate: - `galon` reads → `galon.transactions.view` - `suppliers` reads → `masterdata.suppliers.view` - `cashier/shifts` reads → `cashier.shifts.view` - `cart`, `sales-orders/me`, `pos` (RequireAnyApp POS|ERP), `returns/me`, `delivery-orders/me`, `invoices/me|receivables/me`, `payments/me`, `ecomGate` already gated. - Galon service enforces store-scope on `Get` / `transition` / `ReturnGalon` / `GallonInfo` / `GallonHistory` (fixes cross-store + cross-customer gallon leak). SQL `galon.sql` updated; `sqlc generate` regenerated; tests green. - `docs/openapi.yaml` updated with `galon/by-code/{code}` + `/info`. - `go build` / `go vet` / `go test ./...` pass. ### POS frontend (`keluargamart-pos-frontend`) - Router syntax bug (duplicate `},`) fixed → SPA boots. - `can()` now fail-closed (deny when permissions array absent). - `auth.refresh()` action re-syncs identity/permissions from `GET /auth/me`; router guard self-heals legacy sessions before `isPOS`/permission checks. - Login open-redirect sanitized (`/redirect` must start with `/` and not `//`). - `npm run build` passes. ## Session 2026-08-26 — B2B decisions applied (uncommitted) - P0 fixed: `mapSessionUser()`/mock `makeSession()` now set `applicationAccess`; `login.vue` re-fetches identity via `me()` before the post-login redirect. Admin login un-bricked. - Removed: `AdminRepo.sendNotification` chain (no-op fake success in live mode) incl. `/admin/notifications` page + nav; stubbed `piutangRepo` portfolio feature (page/composable/component/types/mock seed/nav/dashboard card) — its only backend counterpart (`GET /invoices/receivables`, `fakturisasi.*`) is ERP-only, so live mode could never serve it; mock showed OTHER companies' invoices to a customer-side admin = cross-store leak by design. - Fake notification arrays in `layouts/admin.vue`/`account.vue` replaced by an honest link to `/account/notifications` (`AppNotificationsBell` deleted). - Nav decision for flag-only ecommerce_admin: HIDE (option b), not ecomGate — gating masterdata/orders/payments writes would violate main.go:174-177 ("the flag never confers ERP rights"). admin.vue nav now: dashboard, customers, promotions(vouchers) — all backend ecomGate'd. Bottom bar rewired. - Role semantics (user-confirmed): tagihan/hutang = member-scoped own bills — ALREADY served by existing /payments + /invoices pages via /payments/me and /invoices/me (nav label now "Tagihan"). Piutang (portfolio receivables) is an ERP surface; a per-store ecommerce_admin piutang view needs NEW backend work. - ERP MODULES aligned to backend globs: +tracking(admin,sales), purchasing(+warehouse), distribution(-finance), hr/settings(-employee; settings now admin/sales/system per the /settings gate). Konsolidasi nav moved system→reports (its page reads profit-loss). - ERP PosPage: warehouses hydration dropped entirely (was 403 for sales; the ref was never rendered); checkout sends customer_id 0 (walk-in resolves server-side) and omits warehouse_id (backend picks first active). SalesOrdersPage/PurchaseOrdersPage hydrate via GET /warehouses/options. - ERP: auth store gained `permissions` getter + refreshMe() (GET /auth/me EffectivePermissions), invoked fire-and-forget from the router guard; DistributionPage buttons read that map instead of raw user+admin fallback. Topbar notification links point at real routes. LoginPage demo accounts buyer/Kasir removed (always bounced). Fixed pre-existing build break: `Envelope` was imported from vendor/phosphor.js but never exported there. - POS stock.js verdict: warehouse_id STAYS client-supplied — backend /inventory/stock-in|out requires it and does NOT derive tenant server-side (documented in-code). POS LoginPage demo block gated behind import.meta.env.DEV. ## Session 2026-08-26b — CRUD field-level audit applied (uncommitted) Four read-only traces re-run and consumed (CrudB2B, CrudPOS, CrudERP, ErpAudit). FIXED: - P0 B2B admin customer edit wiped master data + deactivated the account (partial PUT against full-replace PUT /customers/{id}). updateCustomer now raw-fetches and overlays via toFullCustomerInput; priceLevel→customer_type (select restricted to retail|wholesale|distributor), region→city, dead `branch` field removed from form. - P0 role 'owner': accepted by validRole/AllRoles but rejected by users_role_check → migration 000039_owner_role.sql (+atlas rehash). - P0 ERP CustomersPage offered regular/member types the backend rejects — CUSTOMER_TYPES aligned to retail|wholesale|distributor, default retail. - P1 catalog UpdateProduct silently NULLed description/image when omitted — now preserves stored values (same contract as is_featured). - P1 partners: negative credit_limit now rejected on create AND update. - P1 POS cart.js called unimported memberService (membership badge never worked) — import fixed. - P1 POS galon createOrder fabricated a "completed" receipt when POST /galon failed — error now propagates to GalonCheckoutDialog's error state; getStatus() no longer returns invented counts. - P1 POS SettingsPage tax/footer toggles PUT keys the backend drops — now honest device-local prefs (localStorage), labeled as such; settings.js PUTs server-first and only caches locally after success. - P1 ERP UsersPage dropped explicit-false permission overrides on resave — tri-state map preserved through openEdit/buildPermissions. - P1 B2B removeUser silent no-op → notImplemented error like its siblings. - P2 B2B checkout poNumber input (never sent) removed; returns order picker filtered to delivered-only (backend rule). STILL OPEN (need design/backend surface, do NOT half-build): - ListUsers ?role=&status= filters ignored (users.sql ListUsers has no predicates) — needs query + handler change + sqlc regen. - users.employee_id select in ERP UsersPage is a dead field (no column/API); employees.user_id write-through undefined. - internal/settings: store fields use '' as keep-sentinel so they can never be cleared; GalonConfig non-pointer ints zero siblings on partial PUT — pointer refactor required. - POS galon drawer collects paymentMethod/amountPaid/deliveryFee/shiftId that TransactionInput cannot record — decide: route galon revenue through POST /pos or extend TransactionInput (shift cash reconciliation gap). - Product units/barcodes subresources have no ERP UI; product `code` field is editable but server-generated (render read-only). ## Session 2026-08-26c — LIVE authorization verification (uncommitted) Real local stack: PostgreSQL 18 + Redis services, DB keluargamart_audit, 39 migrations applied, seed.sql loaded, 3 stores (KM-PST/KM-PLM/KM-BNJ) with per-store memberships; extra identities seeded: owner@ (platform), dev@ (system/developer), ecomadmin@ (buyer+is_ecommerce_admin). Backend :8080, ERP :5173, POS :5174, B2B :3000. P0 FOUND & FIXED LIVE (invisible to unit tests): - internal/auth/repository.go derived every request's ApplicationAccess with the customer e-commerce-admin flag hardcoded false → real ecommerce_admin identities were 403'd on ALL B2B admin routes while /auth/me claimed the elevated scope. Now enriched per load. - DeriveApplicationAccess mapped flagged buyers to plain 'ecommerce' — B2B backoffice was unreachable by definition. Flagged buyer ⇒ ecommerce_admin. - users.application_access (000037) was stored but never read → operator- assigned 'pos' users could not enter the POS shell. Repository now honors a stored 'pos' override. Seeded cashier/kasir.* as 'pos' and verified live. VERIFIED LIVE (direct HTTP + browser): - Login matrix 14 identities → role/application_access correct for all. - Cross-app denials all 401/403: buyer→customers/users/pos/kpis; ecomadmin→sales-orders list/products/users/cashflows; finance/warehouse→ cart & payments/me & checkout; sales→cashflows/employees/users/ purchase-orders; system→pos; employee→pos/users; cashier→users/customers/ hr. ALLOWED exactly as designed: ecomadmin→analytics/vouchers/customers/ kpis/receivables-me; system→users. - Cross-store: kasir PUSAT sees only store-1 orders (list AND detail 404 on foreign); owner PLM only store-2; admin sees both. POST /pos with foreign store_id → 404 (binding resolved from identity, not input). - IDOR: buyer → other customer's order detail denied (403 perm gate); /sales-orders/me excludes foreign orders. - warehouse_id: unknown id rejected (400) by stock-in/out; valid accepted. NOTE: warehouses carry no store binding in the schema, so cross-WAREHOUSE use within the tenant is currently possible by design — document or add warehouses.store_id if store-level isolation is ever required. - Stale sessions: logout revokes server-side (204) and token reuse → 401; garbage token → 401; frontend clears session on 401. - CRUD regression re-check on touched resources via API: partial customer PUT no longer wipes identity fields (frontend sends full input; backend contract remains strict full-replace with required name); product update preserves description/image when omitted; negative credit_limit rejected. ASSESSMENTS (no code change): - ListUsers ?role=&status= filters: LISTING-only; row visibility is gated by system.users.view before filters apply and users have no tenant dimension → stays P1, does not affect authorization. - employee.user_id sync: CreateUser never links employees and CreateEmployee accepts arbitrary/unvalidated UserID (duplicates possible; EmployeeByUserID first-match). Data-integrity P1 — no privilege-escalation path found (permissions derive solely from users.role/permissions). ## Verification gate before "done" - Backend: `go build ./... && go vet ./... && go test ./...` - Each frontend: `npm run build` (or `nuxi build`). - Manual archetype matrix: EXECUTED LIVE 2026-08-26 (see above). Re-run after any auth-layer change: scripts are procedural, DB keluargamart_audit local. ## Note on background scouts Four `scout` agents (ErpAudit, CrudB2B, CrudPOS, CrudERP) were in-flight at checkpoint. If their reports are unavailable in the next session, re-run the same read-only traces — they are stateless and reproducible. ## Session 2026-08-26d — Frontend Production Audit + DB/Env fixes Stack: Postgres km-pg + Redis km-redis, backend :8080 (go run), B2B :3000, ERP :5173, POS :5175. All 4 repos on `authz-audit-2026-08-25`, builds green, `go test ./...` green. ### DB fixes (psql `km-pg` erp) - `users.application_access` `erp → pos` for `cashier@`, `kasir.*`, `owner.*` (6 rows) — POS shell `isPOS === pos|platform` previously blocked every cashier (only `admin=platform` had pos); `POST /pos` via `cashier@pos` now 201, `POST /cart/checkout` via `buyer@ecommerce` → `SO-202608-000038` confirmed. - Verified `sales_orders` 40, `invoices` 17, `products` 36 after frontend e2e. ### Env / runtime - Backend restarted with `CORS_ORIGINS=http://localhost:5173,5174,5175,5199,3000,3001,3003,5176,3002` + `DATABASE_URL` + `REDIS_URL` — previously only `http://localhost:3000` (default), preflight `OPTIONS /api/v1/auth/login` from 5175 returned 403; now 204 with `Allow-Headers: Authorization, Content-Type, X-Request-Id, Idempotency-Key`. - `keluargamart-b2b-frontend/.env` (gitignored) `NUXT_API_PROXY_TARGET=http://localhost:8080` for host dev — previously only `http://backend:8080` (compose-only, host ENOTFOUND). - `keluargamart-erp-frontend/.env.example` vs `.env` map/freshness vars synced. - `keluargamart-pos-frontend/.env` BOM stripped. ### Frontend fixes committed on this branch (see each repo's AUDIT_CONTINUATION) - B2B: checkout `sr-only` radios + `type=button`, cart auth guard, `getRepos()` mock gate. - ERP: upload path `/uploads` bypass, nav reports, mock registry parity, `vite manualChunks` + proxy. - POS: `can()` fail-closed, receipt tax double-add, vite proxy, `.env` docs. ### Verification gate - Backend: `go build ./... && go vet ./... && go test ./...` ✓ - B2B: `bun run build` + `vitest 43/43` ✓ — `curl POST /cart/checkout` via `http://localhost:3000/api/v1` (Nitro) 201. - ERP: `bun run build` + `vitest 14/14` ✓ - POS: `bun run build` + `vitest 8/8` ✓ — Playwright `http://localhost:5175/login` + `http://localhost:5173/login` 200, Chrome screenshots `/tmp/*-login.png`.