# Three sign-in guards on one users table

Until now the site had one guard, `web`, for everyone, and one `users` row per person with one
`user_type`. A phone (and an email) could belong to one row only, so a seller could not shop as a
customer, a customer could not open a shop without their account being turned into a seller's (the
seller OTP sign-in rewrote `user_type` from customer to seller), and signing in anywhere replaced
whoever was signed in before in that browser.

Decided with the user on 2026-11-09 (a grilling round, every recommendation accepted):

- **One `users` table, never one table per role.** About 73 tables and 66 models point at
  `users.id`, and notifications, audits, Sanctum tokens and staff roles store the class name too.
  Splitting would rewrite all of them on live data, break every app session and make ids ambiguous
  (customer #500 vs seller #500) in reports and the management views, for a mostly cosmetic gain.
- **Separate accounts, not one account with several roles.** One person may hold one row per role
  group - customer, seller, admin/staff, delivery boy - with the same phone and email. The rows are
  independent: no link column, no "switch to my shop" button (it can come later). Orders, wallet,
  coupons, cart and addresses stay on the customer row; shop, products and payouts on the seller row.
- **Three guards on that table**, each reading `users` limited to its roles
  (`App\Auth\RoleEloquentUserProvider`): `web` the storefront (customer, delivery boy), `seller` the
  seller panel, `admin` the admin panel (admin and staff share it). Delivery boys get no guard of
  their own; their work is in the app.
- **Each panel makes its guard the default** (`UseRequestGuard`, `AuthGuards::forRequest()`), so the
  ~1,200 existing `auth()->user()` calls, the `auth` / `admin` / `seller` middleware and the views
  read the right account without being touched. Routes both panels share (uploader, notes) use the
  panel that is signed in.
- **One account per browser** (revised the same day, 2026-11-09: the user saw no need for several).
  Signing in to any guard signs out whoever was signed in before, in every guard, quietly (as the
  single guard replaced them: no Logout event, their remember token untouched); logging out ends the
  whole session. A seller who wants to shop signs out of the panel and in with their customer account.
  "Log in as seller / customer" replaces the admin's session, as before; the audit tag
  `impersonating:<id>` is unchanged. The guards stay because they decide which kind of account each
  panel reads: a customer is never signed in to a panel. Outside the panels (storefront, and the
  routes the panels share with it: uploader, logout, language) the request reads whichever account
  is signed in, so a seller browsing the storefront is still the seller, as before: making them a
  guest there would mean listing every shared route, and a missed one breaks a panel page. Allowing several at once later is a change to `AuthGuards::login()` / `logout()` only.
- **Permissions stay on the `web` guard name** (`User::$guard_name`). Spatie 5 otherwise reads
  `auth.defaults.guard`, which the admin panel now sets to `admin`.

## Phases

1. **Guards and sessions, no data change** (2026-11-09). A seller or admin who signs in is still also
   signed in to `web`, as before (a mirror removed in phase 2), and `web` still accepts every row, so
   nothing a seller or admin sees changes. Sessions from before the deploy are carried into their
   guard once (`adoptWebSession()`), so nobody is signed out.
2. **The same phone or email on a seller row and a customer row** (2026-11-09).
   - Uniqueness per role group (`App\Services\Auth\AccountRoles`): every registration and profile
     rule uses `AccountRoles::unique()`, every sign-in / registration lookup `AccountRoles::scope()`.
     The vendor's `users_email_unique` became `users_email_user_type_unique` (migration
     `2026_11_09_000001`, one `ALTER`; `down()` refuses while two accounts share an email). The phone
     never had an index.
   - `web` limited to customers and delivery boys (provider `customer_users`), and the mirror dropped:
     a seller who wants to shop signs in to their customer account (signing out of the panel). Orders
     a seller row placed stay on it. Old sessions and remember-me cookies holding a seller / admin in `web` are
     moved into their guard once (a cookie only when its token checks out).
   - The seller OTP sign-in looks only at seller rows and no longer turns a customer into a seller: a
     customer-only phone goes to seller registration, which opens a seller row and shop beside it.
     Accounts already converted stay as they are; `php artisan accounts:converted-sellers [--list]`
     counts them (read only).
   - The storefront OTP sign-in, the cart's password sign-in and checkout's phone match look only at
     customer rows (a delivery boy signs in in their app). A seller's or staff member's phone no longer
     blocks a guest checkout.
   - `/login` signs in admin and staff only (`LoginController::guard()` is `admin`); a customer or
     seller there gets "invalid" plus a pointer to the phone sign-in, the same text whoever typed what.
   - Google / Apple sign-in (web and app) only ever finds or creates customer rows.
   - A seller's new customer account gets the welcome coupon like any new customer (it is per
     account). Reviving a deactivated account looks only at rows of the same role.
   - Staff may have a customer or seller account with the same phone or email too.
   - "Become a seller" pages accept a signed-in customer; only a signed-in seller is turned away.
   - **Forgot password** (web forms and the app without `user_type`) does not say which account: when an
     email / phone holds several, the code goes to one in a fixed order - admin, staff, customer,
     seller, delivery boy, an account with a password first (`AccountRoles::pickForPasswordReset()`).
     Customers and sellers sign in by OTP, so in practice this reaches the staff account. The app
     narrows it when it sends `user_type`.
   - Customer-facing lookups by phone (custom notifications, WhatsApp campaign names, cart
     retargeting, the customer Excel import) read customer rows only.
   - The WhatsApp order action page runs the seller panel's confirm / unavailable in the seller guard.
3. **The apps.** No app change: they already send `user_type` and the API searches only that type; a
   Sanctum token belongs to one row. A customer-app sign-in with a phone that so far only belongs to a
   seller goes through the normal registration. Left: the app registration rules and testing.

## Consequences

- A new sign-in path must use `AuthGuards::login()` (never `auth()->login()` / `Auth::login()`), a new
  sign-out `AuthGuards::logout()` / `logoutUser()`.
- A customer or seller opening an admin URL is now "not signed in to the admin panel" (login page /
  401, like a guest) instead of a 404; a customer opening the seller panel gets its sign-in page.
- In tests `actingAs()` signs the user in to their own guard (and sets them on `web`, so code called
  directly still finds them) (`Tests\TestCase::be()`).
- A new registration or profile rule for a phone or email uses `AccountRoles::unique()`, never
  `unique:users,...`; a lookup that signs in, registers or resets a password says its role group.
- Deploy phase 2 with its migration: before it, the database still refuses a second account with the
  same email (a 500 instead of the new account).
