# Super-Admin Account Management — Design

**Date:** 2026-05-31
**Status:** Approved
**App:** ECStores (Laravel + Filament v4)

## Problem

The ECStores super-admin panel (`/super`, guard `super_admins`) is accessible only
to manually-seeded accounts. There is no way, from within the panel, to:

- add additional super-admin accounts,
- suspend an account,
- reset another account's password.

The owner wants to create **sub-accounts** that can use the super-admin panel, while
reserving account management (add / suspend / reset password) to the **main owner
account** (`denis.gallant@gmail.com`).

## Decisions (from brainstorming)

- **Permission model:** Owner-only tier. Two roles: `owner` and `admin`. Only owners
  can manage admin accounts. Sub-admins (`admin`) get the full panel **except** the
  account-management page.
- **Password reset:** Owner types a new password directly; saved immediately. No email
  reset-link flow.
- **Suspend effect:** Block login immediately (no separate login banner).
- **Lockout safeguards** (added for safety even though "Owner-only" tier was chosen,
  not the "protected owner" variant): an owner cannot suspend/delete their **own**
  account, and the **last remaining owner** cannot be suspended, deleted, or demoted.

## Architecture

The existing `Tenant` suspend pattern is mirrored for consistency: a nullable
`suspended_at` timestamp with `suspend()` / `reactivate()` / `isSuspended()` helpers
and Filament row actions.

### 1. Database migration

Add to the `super_admins` table:

| Column          | Type                | Notes                                    |
|-----------------|---------------------|------------------------------------------|
| `role`          | string, default `admin` | values: `owner` \| `admin`           |
| `suspended_at`  | nullable timestamp  | non-null = suspended                      |

The same migration marks the existing main account as owner:

```php
DB::table('super_admins')
    ->where('email', 'denis.gallant@gmail.com')
    ->update(['role' => 'owner']);
```

Fallback if that email is not found: mark the oldest account (`min(id)`) as `owner`,
so the system is never left without an owner.

`down()` drops the two columns.

### 2. `SuperAdmin` model (`app/Models/SuperAdmin.php`)

- Add `role` and `suspended_at` to `$fillable`.
- Cast `suspended_at` to `datetime`.
- Helpers:
  - `isOwner(): bool` — `$this->role === 'owner'`
  - `isSuspended(): bool` — `$this->suspended_at !== null`
  - `suspend(): void` — set `suspended_at = now()`, save
  - `reactivate(): void` — set `suspended_at = null`, save
- Update `canAccessPanel()`:

  ```php
  public function canAccessPanel(Panel $panel): bool
  {
      return $panel->getId() === 'superadmin' && ! $this->isSuspended();
  }
  ```

  Filament evaluates `canAccessPanel()` on every request, so this blocks login **and**
  terminates any active session on its next request — no extra middleware needed.

### 3. Filament Resource — `AdminResource`

Location: `app/Filament/SuperAdmin/Resources/Admins/` (matching the existing
`Plans/` and `Tenants/` resource layout). Model: `SuperAdmin`.

- **Navigation:** group `Platform`, a sort value after Plans, icon (e.g.
  `Heroicon::OutlinedUserGroup`). Navigation label: "Admins".
- **Owner-only gate:** override so the whole resource is inaccessible to non-owners:

  ```php
  public static function canAccess(): bool
  {
      return auth()->user()?->isOwner() ?? false;
  }
  ```

  This hides it from the nav and blocks direct-URL access for sub-admins.

#### Form

- `name` — required.
- `email` — required, email, `unique(ignoreRecord: true)`.
- `role` — Select: `owner` / `admin`, default `admin`, required.
- `password` — TextInput, `password()`, `revealable()`:
  - **Create:** required.
  - **Edit:** not required; `dehydrated(fn ($state) => filled($state))` so a blank
    field leaves the existing password unchanged. Model's `hashed` cast handles
    hashing. This is also the "change password" path.

#### Table

Columns: `name`, `email`, `role` (badge — owner = primary/colored, admin = gray),
status (computed: `Suspended` (danger) / `Active` (success)), `created_at`.

Row actions:

- **Edit** — standard `EditAction`.
- **Reset Password** — `Action` with a modal containing one required password field;
  on submit, updates the record's password (hashed cast applies). Convenience wrapper
  over editing.
- **Suspend** — visible when not suspended **and** the row is not the current user and
  not the last owner; calls `suspend()`, sends a warning notification.
- **Reactivate** — visible when suspended; calls `reactivate()`, success notification.
- **Delete** — `DeleteAction`, hidden/blocked when the row is the current user or the
  last owner.

Pages: `index` (ListAdmins), `create` (CreateAdmin), `edit` (EditAdmin) — mirroring
the Plans resource page structure.

### 4. Lockout / safety rules (centralized)

A single helper determines whether a record is the last owner, e.g.
`SuperAdmin::where('role', 'owner')->count() === 1 && $record->isOwner()`. Applied to:

- **Suspend action** — hidden when `$record->is(auth()->user())` or record is last owner.
- **Delete action** — same conditions.
- **Role demotion** — on form save / edit, if changing the last owner's role away from
  `owner`, block with a validation error or notification.

## Out of scope (YAGNI)

- Email-based password reset links.
- Granular per-feature permissions beyond the two tiers.
- Audit logging of admin-account changes (could be added later if needed).
- Any change to sub-admins' access to Tenants, Plans, Queue Monitor, etc. — they keep
  full access to everything except the Admins resource.

## Testing

- Migration up/down runs cleanly; existing owner account is flagged `owner`.
- Suspended admin cannot log in; an admin suspended mid-session is ejected on next
  request.
- Sub-admin (`admin` role) sees no "Admins" nav item and gets 403 on the direct URL.
- Owner can create an admin, edit it, reset its password (login works with the new
  password), suspend/reactivate it, and delete it.
- Owner cannot suspend/delete their own account.
- The last owner cannot be suspended, deleted, or demoted.
- Blank password on edit leaves the existing password intact.
