# Permissions & Access Control — TinyPOS (`tnx-pos`)

> Produced by the Reversa **Detective** (phase: interpretation) · doc_level: `complete`
> Generated on 2026-09-18

**Confidence scale:** 🟢 CONFIRMED (read directly from code) · 🟡 INFERRED (pattern-based, may be wrong) · 🔴 GAP (needs human validation)

## Summary

TinyPOS has exactly **one class of user**: the back-office **Administrator** (`admin_users`, an Encore\Admin `Administrator`). There is **no customer-facing login** — customers are CRM records, not accounts. Access control is therefore a single decision: *authenticated admin, or not.*

The `salipropham/laravel55-admin` package ships a full table-driven RBAC model (roles, permissions, per-route permission strings, menu ACL), but the application **does not enforce it at the route layer**. All application routes are protected only by the `admin` **auth** guard, not by the package's permission middleware. In practice every authenticated administrator can reach every screen and action. 🟢

---

## How access is actually enforced

All application routes live in one group:

```php
Route::group([
    'prefix'     => config('admin.route.prefix'),   // (admin prefix)
    'middleware' => config('admin.route.middleware'),// ['web', 'admin']
], function (Router $router) {
    $router->resource('/products',  'ProductController');
    $router->resource('/customers', 'CustomerController');
    $router->resource('/orders',    'OrderController');
    $router->resource('/pos',       'PosController');
    $router->resource('/dashboard', 'DashboardController');
    Route::prefix('settings')->group(function (Router $router) {
        $router->resource('/brand',      'BrandController');
        $router->resource('/categories', 'CategoryController');
        $router->resource('/units',      'UnitController');
        $router->resource('/gifts',      'GiftController');
    });
});
```

— `routes/web.php:20-91` 🟢

The middleware stack is `['web','admin']` (`config/admin.php:29`). The `admin` guard uses the session driver with provider → `Encore\Admin\Auth\Database\Administrator` (`config/admin.php:51-64`). Crucially, the stack contains the **authentication** middleware but **not** `admin.permission` (the package's per-route permission check). No route or controller declares a `Permission::check`, policy, gate, or `can:` middleware anywhere in `app/`. 🟢

**Consequence:** authorization collapses to authentication. Any logged-in administrator has full CRUD over products, customers, orders, POS, debts, and all reference data. The RBAC tables are seeded and present but decorative from the app's perspective. 🟡 (interpretation of the missing enforcement — confirm with the team whether this is intended for a single-operator shop.)

---

## Seeded roles & permissions

The default RBAC data comes from the vendored `Encore\Admin\Auth\Database\AdminTablesSeeder` (invoked by `database/seeds/DatabaseSeeder.php`). It creates a single super-role. 🟢

### Roles

| Role | Slug | Notes | Confidence |
|------|------|-------|------------|
| Administrator | `administrator` | The only seeded role; assigned the `*` permission. | 🟢 |

### Permissions (seeded)

| Permission | Slug | HTTP method | Path | Confidence |
|------------|------|-------------|------|------------|
| All permission | `*` | (any) | `*` | 🟢 |
| Dashboard | `dashboard` | GET | `/` | 🟢 |
| Login | `auth.login` | (any) | `/auth/login`, `/auth/logout` | 🟢 |
| User setting | `auth.setting` | GET, PUT | `/auth/setting` | 🟢 |
| Auth management | `auth.management` | (any) | `/auth/roles`, `/auth/permissions`, `/auth/menu`, `/auth/logs` | 🟢 |

The seeded Administrator role holds only the first permission (`*`), which subsumes the rest. These permissions govern the **package's own** `/auth/*` admin screens; they are not wired to the application's `/products`, `/orders`, etc. routes. 🟢

### Default account

`AdminTablesSeeder` creates a single user `admin` / `admin` (bcrypt). 🟢 Confirmed with the team: the production password has already been rotated away from this default — the risk is theoretical (only affects a fresh seed/reset), not a live exposure.

---

## Effective permission matrix

Because enforcement is authenticate-only, the matrix has two columns. "Administrator" = any authenticated admin user (all of them, since only one role exists). 🟢

| Capability | Anonymous | Administrator | Confidence |
|------------|:---------:|:-------------:|------------|
| Access any application route | ❌ (redirected to login) | ✅ | 🟢 |
| POS: build cart, submit order (`/pos`, `/orders`) | ❌ | ✅ | 🟢 |
| Orders: create / edit / delete | ❌ | ✅ | 🟢 |
| Products: CRUD, unit setup, pricing | ❌ | ✅ | 🟢 |
| Customers: CRUD, quick-add, statistics | ❌ | ✅ | 🟢 |
| Loyalty: redeem gift, adjust points | ❌ | ✅ | 🟢 |
| Debts: record debt / repayment, view `/debts` | ❌ | ✅ | 🟢 |
| Reference data: brands, categories, units, gifts | ❌ | ✅ | 🟢 |
| Dashboard / KPIs | ❌ | ✅ | 🟢 |
| Encore\Admin `/auth/*` (users, roles, permissions, menu, logs) | ❌ | ✅ (via `*`) | 🟢 |

There is **no** finer-grained separation (e.g. cashier vs manager) in code. Introducing one would mean adding roles/permissions **and** wiring the package's permission middleware into the route group — neither exists today. 🟡

---

## Non-route restrictions (business-rule guards)

These are not RBAC, but they are the only *within-role* access limits that exist. They gate **actions**, not **users**. 🟢

| Guard | Rule | Location | Confidence |
|-------|------|----------|------------|
| Order editability | Edits/finalisation blocked once a `done` order is >24h old (`is_editable`). | `Order::is_editable` | 🟢 |
| Debt lock | Debt input disabled and server-ignored once a `pos_debt` row exists (`debt_locked`). | `Order`, `PosController:549-554` | 🟢 |
| Debt input requires customer | `debt_amount>0` requires a selected customer and `≤ total`. | `OrderController`, `PosController:308-334` | 🟢 |
| Repayment ceiling | Repayment cannot exceed `debt_total`. | `CustomerDebt::record` | 🟢 |
| Gift redemption gate | `checkGiftAvailable`: stock, per-customer `limit`, point balance. | `Customer::checkGiftAvailable` | 🟢 |
| Protected reference rows | Delete disabled on all brand/category/unit rows; brand id 1 & unit id 1 edit-locked (category id-1 lock commented out — GAP-C2). | Brand/Unit/Category controllers | 🟢 |

---

## Gaps (resolved 2026-09-18, one informational note remains)

- **PERM-1 — RBAC present but unenforced.** ✅ Confirmed with the team (2026-09-18): intentional — this is a single-operator shop, no cashier/owner role separation is needed. The permission model remains seeded but decorative from the app's perspective. 🟢
- **PERM-2 — Default `admin`/`admin` credential.** ✅ Confirmed with the team (2026-09-18): production password has already been rotated away from this seeded default. Residual risk is theoretical only — anyone re-running the seeder (fresh env, DB reset) would recreate it, so it's worth keeping in mind for onboarding/runbooks, but not an active exposure. 🟢
- **PERM-3 — No app-level policies/gates.** No Laravel Gates, Policies, or `can:` middleware anywhere in `app/`. Any future authorization would start from zero. 🟡
