chatRAG / multi_admin_implementation_plan_backup.md
joedown11's picture
feat: claim-based escalation, admin transfer, per-admin WhatsApp, profile settings
f672bfd
|
Raw
History Blame Contribute Delete
56 kB
# Multi-Admin RBAC β€” Full Implementation Plan
## Background
Currently, the admin panel has exactly **one** hardcoded admin (from `.env`). Authentication is a 2-step flow: password check β†’ OTP β†’ HTTP Basic Auth on all subsequent API calls. Credentials are stored in `localStorage`. OTP always goes to `settings.admin_email` (the global one).
This plan adds a **multi-admin system** with a `superadmin` role and restricted sub-admin accounts (e.g. "HR"), each with selective permissions, their own email, their own password, and the ability to change their own password.
---
## Open Questions (Please Answer Before Execution)
> [!IMPORTANT]
> **Password Hashing Library**
> To securely store passwords, we need hashing. Two options:
> - **Option A (No new deps):** Use Python's built-in `hashlib.pbkdf2_hmac` with a random salt (SHA-256, 260,000 iterations). This is NIST-approved and requires zero new dependencies.
> - **Option B (Add `bcrypt`):** Add `bcrypt` (or `passlib[bcrypt]`) to `requirements.txt`. More commonly recognized in industry.
>
> **Recommendation: Option A** to keep things self-contained. Please confirm.
> [!IMPORTANT]
> **Session Authentication: Replace HTTP Basic Auth?**
> Currently, the frontend stores raw username+password in `localStorage` and sends them as an HTTP Basic Auth header on every single API call. In a multi-admin world this is still functional, but it means the raw password is always in memory and in every request header.
>
> A more modern approach: after OTP verification, issue a **short-lived token** (a signed HMAC token stored in the `admins.json`). The frontend stores this token, not the password. On logout or after 8 hours it expires automatically.
>
> **Recommendation: Keep HTTP Basic Auth** for backward-compatibility and simplicity (it already works), but add a `token` field to the store for logout invalidation. Please confirm.
> [!IMPORTANT]
> **Permissions Scope**
> The plan proposes 5 granular permissions. Please confirm or adjust:
> - `manage_sessions` β€” View sessions, reply, block/unblock, takeover, end chat, archive/unarchive, delete
> - `manage_whatsapp` β€” View and modify WhatsApp settings, test connection, fetch QR
> - `view_analytics` β€” View dashboard metrics, SLA dashboard, charts, export data
> - `manage_kb` β€” Future-proofing for knowledge base / document management
> - `manage_admins` β€” Create, edit, delete other admin accounts (**Superadmin only by default**)
---
## Permission Architecture β€” Two-Level Hierarchy
The permission system has **two levels**. Every piece of UI the superadmin can grant access to maps to one of these levels.
### Level 1 β€” Top-Level Permissions (Sidebar Items)
Each Level 1 permission controls **whether a sidebar nav item is visible at all**.
| Sidebar Nav Item | `id` | Level 1 Permission Required |
|---|---|---|
| Dashboard | `nav-dashboard` | `view_analytics` |
| SLA Dashboard | `nav-sla` | `view_analytics` |
| Live Sessions | `nav-sessions` | `manage_sessions` |
| Queued Users | `nav-queued` | `manage_sessions` |
| Daily Users | `nav-daily` | `manage_sessions` |
| Blocked Users | `nav-blocked` | `manage_sessions` |
| Archived | `nav-archived` | `manage_sessions` |
| Settings | `nav-settings` | At least one settings sub-permission |
| Admin Users *(new)* | `nav-admin-users` | `manage_admins` |
| Chat Widget Demo *(quick link)* | β€” | Any permission (always shown) |
> [!NOTE]
> Settings is a **composite** nav item. It is visible only if the sub-admin has at least one Settings sub-permission (e.g. `manage_whatsapp`). It is **hidden entirely** if they have zero settings sub-permissions.
---
### Level 2 β€” Sub-Permissions (Sections Within a Sidebar Item)
Each Level 2 sub-permission controls **a specific section or action inside a top-level view**.
#### Under `manage_sessions` (Live Sessions view)
| Sub-Permission | What it controls |
|---|---|
| `sessions.view` | Can see the session list and read transcripts |
| `sessions.reply` | Can type and send replies in the reply area |
| `sessions.takeover` | Can click "Take Over" and start a human takeover |
| `sessions.block` | Can block/unblock users |
| `sessions.end_chat` | Can end a chat completely |
| `sessions.archive` | Can archive/unarchive sessions and chat threads |
| `sessions.delete` | Can delete sessions and individual chat threads |
> [!NOTE]
> `sessions.view` is always required for any other `sessions.*` sub-permission to make sense. If only `sessions.view` is granted, the sub-admin sees read-only transcripts with no action buttons.
#### Under `view_analytics` (Dashboard & SLA views)
| Sub-Permission | What it controls |
|---|---|
| `analytics.dashboard` | Sees the main Dashboard view (metrics + chart) |
| `analytics.sla` | Sees the SLA Dashboard view |
| `analytics.export` | Sees the "Export Data" button and can download reports |
#### Under `manage_whatsapp` (Settings β†’ WhatsApp section)
| Sub-Permission | What it controls |
|---|---|
| `whatsapp.view` | Can view current WhatsApp config (numbers, provider) |
| `whatsapp.edit` | Can modify and save WhatsApp settings |
| `whatsapp.test` | Can click "Send Test Message" |
| `whatsapp.qr` | Can fetch and view the Baileys QR code |
#### Under Settings (general sub-sections)
| Sub-Permission | What it controls |
|---|---|
| `settings.notifications` | Can view and toggle the Notifications settings sub-section |
| `settings.config` | Can view and toggle the Config settings sub-section (auto-refresh) |
| `settings.security` | Can view the Security sub-section (OTP status) |
| `settings.version` | Can view the Version Info sub-section |
> [!NOTE]
> `settings.whatsapp` is controlled by the `whatsapp.*` sub-permissions above β€” not as a standalone settings sub-permission. This avoids duplication.
#### Under `manage_admins` (Admin Users view)
| Sub-Permission | What it controls |
|---|---|
| `admins.view` | Can view the list of all admin accounts |
| `admins.create` | Can create new admin accounts |
| `admins.edit` | Can edit existing admin accounts (permissions, email, role) |
| `admins.delete` | Can delete admin accounts |
| `admins.reset_password` | Can reset another admin's password |
> [!CAUTION]
> `manage_admins` as a whole is **restricted to Superadmins by default**. Even if granted to a sub-admin (e.g. a "Senior HR Manager"), the sub-admin cannot grant permissions or roles they don't already have themselves β€” this is enforced on the backend.
---
### Sub-Permission Rules
26. Sub-permissions are **nested under their parent**. A sub-admin cannot have `sessions.reply` without also having `sessions.view` β€” the backend rejects such combinations.
27. When a parent-level permission toggle is turned **OFF** in the admin modal, all its child sub-permissions are **also cleared** simultaneously. The sub-permission toggles become hidden.
28. When a parent-level permission toggle is turned **ON**, the sub-permission group **expands** beneath it with individual toggles, all defaulting to **OFF** (except `sessions.view` which auto-toggles ON when `manage_sessions` is enabled, since you can't have sessions without viewing them).
29. Each sub-permission group has a **"Select All" / "Clear All"** shortcut link next to the group header for quick bulk-toggling.
30. The backend enforces sub-permissions independently β€” a missing sub-permission returns `403` even if the parent permission is present. Frontend hiding is a UX convenience, **not** the security layer.
31. Superadmins have **all sub-permissions** implicitly. Their `permissions` field in `admins.json` stores only the parent-level keys; sub-permissions for superadmins are implied.
32. Sub-permissions are stored as **flat strings** in the `permissions` array, using dot notation: `["sessions.view", "sessions.reply", "analytics.dashboard"]`. This keeps the JSON schema simple with no nested objects.
---
### Superadmin Rules
1. There must **always be at least one active superadmin**. Deleting or demoting the last superadmin is forbidden.
2. A superadmin **cannot delete or demote themselves** if they are the last superadmin.
3. A superadmin **cannot delete their own account** while logged in (to prevent accidental lockout).
4. Superadmins have **all permissions** implicitly and cannot have permissions individually revoked.
5. The initial superadmin is seeded from `.env` (`ADMIN_USER` / `ADMIN_PASS`) at first startup. After seeding, `.env` credentials are **still checked as a fallback** (so you're never locked out).
### Sub-Admin Rules
6. Sub-admins can only access API routes they have permission for. Attempting to access a route without the required permission returns **403 Forbidden** (not 401).
7. A sub-admin **cannot grant themselves permissions** they do not have.
8. A sub-admin **cannot create, edit, or delete other admins** unless they have `manage_admins`.
9. A sub-admin **cannot change another user's password** β€” only their own.
10. A sub-admin's email must be **unique** in the store. Duplicate emails are rejected.
### Permission Toggle Rules (During Creation)
11. When creating a sub-admin, **all permission toggles default to OFF** β€” no permissions are granted unless explicitly turned on by the superadmin.
12. The **role selector** (`Superadmin` / `Sub-admin`) controls toggle availability:
- If `Superadmin` is selected: all toggles are **hidden** (replaced by "All permissions granted" label).
- If `Sub-admin` is selected: all permission toggles are shown and individually controllable.
13. A new sub-admin must have **at least one permission enabled** before the form can be submitted. Attempting to save with zero permissions shows a validation error inline: "Please enable at least one permission."
14. The superadmin performing the creation **cannot grant a permission they don't have themselves** (enforced on backend too β€” rule 7 above).
### Permission Toggle Rules (After Creation β€” Live Editing)
15. A superadmin can open any existing sub-admin's profile at any time and toggle permissions **on or off individually** using the same toggle UI.
16. Toggling a permission in the edit modal does **not auto-save** β€” the superadmin must click "Save Changes" to commit.
17. If a superadmin turns **all toggles off** for an existing sub-admin and tries to save, the backend rejects it with a `400` error: "A sub-admin must have at least one permission."
18. Saving permission changes takes effect **immediately on the backend** β€” the updated `admins.json` is written atomically. The affected sub-admin sees the change the next time their session re-validates (`GET /api/admin/me`).
19. A permission that is **currently being used** (e.g., the sub-admin is actively viewing a sessions list) is not interrupted mid-session. The restriction applies to the **next API call** after the change is saved.
20. The edit modal shows a **read-only "last modified"** timestamp so the superadmin knows when permissions were last changed.
### Permission Toggle β€” UI Behaviour Rules
21. Each toggle has a **label** (the feature name) and a **description** (one line explaining what access it grants). This removes ambiguity.
22. Toggling a permission in the UI gives an **instant visual response** β€” the toggle animates to the new state immediately, before saving.
23. The "Save Changes" button is **disabled and grayed out** until at least one toggle state has been changed from the original saved state (dirty-state detection).
24. If the superadmin closes the edit modal without saving, a **discard confirmation** is shown: "You have unsaved changes. Discard them?" β€” prevents accidental permission loss.
25. Toggling to `Superadmin` role while editing shows a **warning banner** inside the modal: "Upgrading to Superadmin grants full unrestricted access. This cannot be scoped." The Save button requires a second click to confirm after this warning appears.
### Password Rules
11. Passwords must be **at least 8 characters**.
12. When changing password, the **current password must be verified** before the new password is set.
13. Passwords are stored as `pbkdf2_hmac` hash+salt β€” never in plaintext.
14. The superadmin seeded from `.env` uses the **raw `.env` password for its first login**, then the store takes over. On seeding, the `.env` password is hashed and stored.
### OTP Rules
15. OTP is per-user, sent to **their own email** (not the global `settings.admin_email`).
16. If an admin has **no email configured**, the OTP is only logged to the server console (existing fallback behavior).
17. OTP codes expire in **5 minutes** (unchanged).
18. After 5 failed OTP attempts, the OTP is **invalidated** and a new one must be requested.
19. OTP is **single-use** β€” verified once and immediately deleted from the in-memory store.
### Brute-Force / Security Rules
20. After **5 consecutive failed login attempts** (wrong password at the `/request-otp` stage), the account is **temporarily locked for 15 minutes**. Lock state is in-memory (resets on server restart).
21. All credential comparisons use `secrets.compare_digest()` to prevent timing attacks.
22. Admin usernames are **case-insensitive** for lookup but stored in their original casing.
23. Usernames must be **alphanumeric + underscores only** (`^[a-zA-Z0-9_]{3,32}$`). This prevents path-traversal issues in future file-based or URL-based lookups.
### Data Storage Rules
24. Admin data is stored in a **`data/admins.json`** file (same persistent volume as sessions).
25. File writes use an **atomic write** pattern (write to `.tmp`, then `os.replace()`). This prevents data corruption if the server crashes mid-write.
26. All reads/writes to `admins.json` are wrapped in an **`asyncio.Lock`** to prevent race conditions.
27. The file is **not** exposed via any API endpoint. The `/api/admin/users` endpoint returns only safe fields (`username`, `role`, `email`, `permissions`, `created_at`, `last_login`). The password hash/salt are never returned.
---
## Proposed Changes
---
### 1. `app/services/admin_store.py` β€” [NEW]
This is the core persistence layer. It is the **single source of truth** for all admin accounts.
**Data structure in `admins.json` (updated to reflect sub-permissions + ease features):**
```json
{
"admins": [
{
"username": "martech_admin",
"role": "superadmin",
"email": "admin@example.com",
"display_name": "Martech Admin",
"notes": "",
"password_hash": "...",
"password_salt": "...",
"password_changed_at": "2026-06-12T00:00:00Z",
"permissions": [
"manage_sessions",
"manage_whatsapp",
"view_analytics",
"manage_admins",
"manage_kb"
],
"is_active": true,
"created_at": "2026-06-12T00:00:00Z",
"created_by": "system",
"last_login": null,
"last_active": null,
"failed_attempts": 0,
"locked_until": null,
"force_logout_at": null
},
{
"username": "hr_user",
"role": "sub_admin",
"email": "hr@company.com",
"display_name": "HR Manager",
"notes": "Handles all live chat sessions for the HR department.",
"password_hash": "...",
"password_salt": "...",
"password_changed_at": "2026-06-12T00:00:00Z",
"permissions": [
"manage_sessions",
"sessions.view",
"sessions.reply",
"sessions.takeover",
"analytics.dashboard"
],
"is_active": true,
"created_at": "2026-06-12T00:00:00Z",
"created_by": "martech_admin",
"last_login": null,
"last_active": null,
"failed_attempts": 0,
"locked_until": null,
"force_logout_at": null
}
],
"audit_log": [
{
"timestamp": "2026-06-12T10:00:00Z",
"actor": "martech_admin",
"action": "create_admin",
"target": "hr_user",
"detail": "Created sub-admin with permissions: manage_sessions, sessions.view, sessions.reply"
}
]
}
```
> [!NOTE]
> Superadmins store only the 5 top-level parent keys. Sub-admins store both the parent key (e.g. `"manage_sessions"`) **and** the specific sub-permission keys (e.g. `"sessions.view"`). The parent key's presence is used for sidebar visibility; the sub-key's presence is used for in-view action enforcement.
**Functions to implement:**
| Function | Description |
|---|---|
| `_load_admins()` | Read `admins.json`, return dict. Thread-safe. |
| `_save_admins(data)` | Atomic write to `admins.json`. Uses temp file + `os.replace()`. |
| `get_admin(username)` | Return a single admin record (case-insensitive lookup). Returns `None` if not found. |
| `get_all_admins()` | Return list of all admins (safe fields only, no hash/salt). |
| `create_admin(...)` | Validate inputs, hash password, insert record. Raise on duplicate username/email. |
| `update_admin(username, ...)` | Update role/email/permissions. Enforce superadmin count rules. |
| `delete_admin(username, requesting_user)` | Delete admin. Enforce self-deletion and last-superadmin rules. |
| `verify_password(username, password)` | Verify password against stored hash. Also handles `.env` fallback for the initial superadmin. Increments `failed_attempts` on failure, locks after 5. |
| `change_password(username, old_pass, new_pass)` | Verify old password first, then update hash. |
| `record_login(username)` | Update `last_login` timestamp. |
| `check_locked(username)` | Return `True` if account is locked. |
| `hash_password(password)` β†’ `(hash, salt)` | `os.urandom(32)` salt + `pbkdf2_hmac('sha256', ...)` |
| `seed_initial_admin()` | **Called once at startup.** If `admins.json` does not exist or is empty, create it with the superadmin from `.env`. |
---
### 2. `app/admin/router.py` β€” [MODIFY]
**Auth dependency changes:**
- **`verify_admin(credentials)`** β†’ now calls `admin_store.verify_password()`. Still returns `username` on success. Returns 423 (Locked) if account is locked, 401 otherwise.
- **NEW: `require_permission(permission_name)` factory** β†’ Returns a FastAPI dependency that checks `admin_store.get_admin(username).permissions` and raises **403** if the permission is missing. Superadmins always pass.
- All existing routes that need protection will use **both** `verify_admin` AND `require_permission(...)` where appropriate.
**OTP endpoint changes:**
- `request_otp`: Look up user from `admin_store`. If found, send OTP to **their own email**. If user not found β†’ return generic `401` (don't reveal user existence). If account locked β†’ return `423`.
- `verify_otp`: After success, call `admin_store.record_login(username)` and reset `failed_attempts` to 0.
- Add **OTP attempt counter** per username in `_otp_store`. After 5 wrong OTP guesses, invalidate the OTP entry and force a new login.
**Permission enforcement β€” extended to sub-permission level:**
| Route | Top-Level Permission | Sub-Permission Required |
|---|---|---|
| `GET /api/admin/sessions` | `manage_sessions` | `sessions.view` |
| `GET /api/admin/sessions/{id}` | `manage_sessions` | `sessions.view` |
| `DELETE /api/admin/sessions/{id}` | `manage_sessions` | `sessions.delete` |
| `POST /api/admin/sessions/{id}/block` | `manage_sessions` | `sessions.block` |
| `POST /api/admin/sessions/{id}/unblock` | `manage_sessions` | `sessions.block` |
| `POST /api/admin/sessions/{id}/takeover` | `manage_sessions` | `sessions.takeover` |
| `POST /api/admin/sessions/{id}/end_takeover` | `manage_sessions` | `sessions.takeover` |
| `POST /api/admin/sessions/{id}/reply` | `manage_sessions` | `sessions.reply` |
| `POST /api/admin/sessions/{id}/end_chat` | `manage_sessions` | `sessions.end_chat` |
| `POST /api/admin/sessions/{id}/archive` | `manage_sessions` | `sessions.archive` |
| `POST /api/admin/sessions/{id}/unarchive` | `manage_sessions` | `sessions.archive` |
| `DELETE /api/admin/sessions/{id}/chats/{chat_id}` | `manage_sessions` | `sessions.delete` |
| `GET /api/admin/metrics` | `view_analytics` | `analytics.dashboard` |
| `GET /api/admin/analytics` | `view_analytics` | `analytics.dashboard` |
| `GET /api/admin/sla-metrics` | `view_analytics` | `analytics.sla` |
| `GET /api/admin/export/*` | `view_analytics` | `analytics.export` |
| `GET /api/admin/settings/whatsapp` | `manage_whatsapp` | `whatsapp.view` |
| `POST /api/admin/settings/whatsapp` | `manage_whatsapp` | `whatsapp.edit` |
| `POST /api/admin/settings/whatsapp/test` | `manage_whatsapp` | `whatsapp.test` |
| `GET /api/admin/settings/whatsapp/baileys/qr` | `manage_whatsapp` | `whatsapp.qr` |
| `GET /api/admin/users` | `manage_admins` | `admins.view` |
| `POST /api/admin/users` | `manage_admins` | `admins.create` |
| `PUT /api/admin/users/{username}` | `manage_admins` | `admins.edit` |
| `DELETE /api/admin/users/{username}` | `manage_admins` | `admins.delete` |
| `POST /api/admin/change-password` | *(any logged-in user)* | *(no sub-perm required)* |
| `GET /api/admin/me` | *(any logged-in user)* | *(no sub-perm required)* |
**New endpoints to add:**
```
GET /api/admin/me β†’ Returns current user profile + permissions (no auth dependency beyond verify_admin, used for frontend RBAC)
GET /api/admin/users β†’ List all admins [requires manage_admins]
POST /api/admin/users β†’ Create admin [requires manage_admins]
PUT /api/admin/users/{username} β†’ Update admin role/email/permissions [requires manage_admins]
DELETE /api/admin/users/{username} β†’ Delete admin [requires manage_admins]
POST /api/admin/change-password β†’ Change own password [requires verify_admin only]
```
**Edge cases for new endpoints:**
- `DELETE /api/admin/users/{username}`: Return `400` with message "Cannot delete the last superadmin." if rule is violated. Return `403` if a non-superadmin tries this. Return `400` if trying to delete self.
- `POST /api/admin/users`: Validate username regex `^[a-zA-Z0-9_]{3,32}$`. Return `409` on duplicate username/email.
- `PUT /api/admin/users/{username}`: If changing `role` from `superadmin` to `sub_admin`, check remaining superadmins first. A sub-admin cannot set permissions higher than their own.
- `POST /api/admin/change-password`: Validate new password is >= 8 chars. Must verify old password first.
---
### 3. `app/main.py` β€” [MODIFY]
**Startup hook:**
- Call `admin_store.seed_initial_admin()` at application startup (in the `lifespan` context or `@app.on_event("startup")`).
- This is safe to call on every restart β€” it only creates the file if it doesn't already exist.
---
### 4. `app/admin/templates/admin.html` β€” [MODIFY]
This is the largest change on the frontend. All changes are purely additive or guarded so existing admins see no difference.
#### A. Post-Login Permission Fetching
- Immediately after `initApp()` is called (after OTP verified), call `GET /api/admin/me`.
- Store the result in a global `window._adminProfile = { username, role, permissions, email }`.
- Call a new `applyPermissions()` function.
#### B. `applyPermissions()` Function β€” Two-Level Rendering
This function runs once after `GET /api/admin/me` returns. It performs two passes:
**Pass 1 β€” Sidebar item visibility (Level 1):**
| Condition | Action |
|---|---|
| Does NOT have `view_analytics` | Hide `nav-dashboard`, `nav-sla` |
| Does NOT have `manage_sessions` | Hide `nav-sessions`, `nav-queued`, `nav-daily`, `nav-blocked`, `nav-archived` |
| Does NOT have any settings sub-permission | Hide `nav-settings` entirely |
| Does NOT have `manage_admins` | Hide `nav-admin-users` |
**Pass 2 β€” In-view element visibility (Level 2, sub-permissions):**
This pass runs every time the user navigates to a view, or on initial load.
*Within the Sessions view (`manage_sessions` granted):*
| Sub-Permission Absent | Elements Hidden / Disabled |
|---|---|
| `sessions.reply` | `#chat-reply-area` (reply input + Send button) hidden |
| `sessions.takeover` | `#btn-takeover`, `#btn-end-takeover` hidden |
| `sessions.block` | `#btn-block-user` hidden; "Block User" in context menu hidden |
| `sessions.end_chat` | `#btn-end-chat` hidden |
| `sessions.archive` | "Archive" and "Unarchive" context menu items hidden |
| `sessions.delete` | `#btn-delete-current-chat`, `#btn-delete-session` hidden; "Delete" context menu item hidden |
*Within the Analytics views (`view_analytics` granted):*
| Sub-Permission Absent | Elements Hidden |
|---|---|
| `analytics.dashboard` | `nav-dashboard` hidden; if they navigate directly, redirect to first allowed view |
| `analytics.sla` | `nav-sla` hidden |
| `analytics.export` | `#btn-open-export` (topbar Export button) hidden; `#btn-export-single` (inside chat header) hidden |
*Within the Settings view (`nav-settings` visible):*
| Sub-Permission Absent | Elements Hidden |
|---|---|
| `settings.notifications` | `data-settings-sec="notifications"` nav item hidden; section hidden |
| `settings.config` | `data-settings-sec="config"` nav item hidden; section hidden |
| `settings.security` | `data-settings-sec="security"` nav item hidden; section hidden |
| `settings.version` | `data-settings-sec="version"` nav item hidden; section hidden |
| `whatsapp.view` | `data-settings-sec="whatsapp"` nav item hidden; section hidden |
| `whatsapp.edit` | Save Settings button disabled/hidden; inputs become read-only |
| `whatsapp.test` | `#btn-test-whatsapp` hidden |
| `whatsapp.qr` | QR Code fetch button hidden |
*Within the Admin Users view (`manage_admins` granted):*
| Sub-Permission Absent | Elements Hidden |
|---|---|
| `admins.create` | "New Admin" button hidden |
| `admins.edit` | Edit (pencil) icon hidden in every table row |
| `admins.delete` | Delete (trash) icon hidden in every table row |
| `admins.reset_password` | "Reset Password" button hidden inside the edit modal |
**Pass 2 default view redirect:**
- After hiding elements, if the currently active view has become empty or inaccessible, the app automatically switches to the **first visible nav item**.
- Order of fallback: Sessions β†’ Dashboard β†’ SLA β†’ Settings β†’ Admin Users.
- If **no** views are accessible at all (zero permissions), show a full-screen empty state: "You don't have access to any sections. Contact your superadmin."
#### C. Welcome Toast Update
- Change hardcoded "Welcome back, Admin!" β†’ "Welcome back, {username}!" using `window._adminProfile.username`.
#### D. Admin Users Management View (new `id="view-admin-users"`)
A full CRUD table, only visible to `manage_admins` users:
- **Table columns:** Username, Role badge (Superadmin / Sub-admin), Email, Active Permissions (pill badges), Last Login, Actions (Edit, Delete).
- **"New Admin" button:** Opens the Create Admin modal.
- **Edit action (pencil icon):** Opens the Edit Admin modal, pre-filled with current values.
- **Delete action (trash icon):** Shows confirmation dialog. Button is **grayed out and disabled** if target is the last superadmin.
- **Quick-toggle column (optional UX enhancement):** Each sub-admin row shows tiny lock/unlock icons for each permission directly in the table, allowing rapid one-click toggling without opening the full edit modal. Superadmin rows show a "πŸ”‘ Full Access" badge instead.
---
#### D1. Create Admin Modal
Triggered by the "New Admin" button. Fields:
| Field | Type | Notes |
|---|---|---|
| Username | Text input | Required. Validated: `^[a-zA-Z0-9_]{3,32}$`. Read-only once created. |
| Email | Email input | Required. Must be unique across all admins. |
| Password | Password input | Required on create only. Min 8 chars. |
| Role | Radio buttons | `Superadmin` or `Sub-admin`. Defaults to `Sub-admin`. |
| Permissions | Toggle group | Only shown when `Sub-admin` is selected (see below). |
**Permission Toggle Group (create mode β€” Two Levels):**
The create modal shows **collapsible permission groups**. Each group is a parent toggle at the top, with child sub-permission toggles that expand beneath it.
```
[ β–Ά πŸ’¬ Live Sessions ] ← parent toggle, OFF by default
↳ (expands when parent is ON)
[ ] View transcripts (sessions.view β€” auto-ON when parent enabled)
[ ] Send replies (sessions.reply)
[ ] Take over chat (sessions.takeover)
[ ] Block / Unblock users (sessions.block)
[ ] End chat completely (sessions.end_chat)
[ ] Archive sessions (sessions.archive)
[ ] Delete sessions (sessions.delete)
[ β–Ά πŸ“Š Analytics & Export ] ← parent toggle, OFF by default
↳ (expands when parent is ON)
[ ] Main Dashboard (analytics.dashboard β€” auto-ON when parent enabled)
[ ] SLA Dashboard (analytics.sla)
[ ] Export data (analytics.export)
[ β–Ά πŸ“± WhatsApp Settings ] ← parent toggle, OFF by default
↳ (expands when parent is ON)
[ ] View config (whatsapp.view β€” auto-ON when parent enabled)
[ ] Edit & save config (whatsapp.edit)
[ ] Send test message (whatsapp.test)
[ ] Fetch QR code (whatsapp.qr)
[ β–Ά βš™οΈ Settings Sections ] ← parent toggle, OFF by default
↳ (expands when parent is ON)
[ ] Notifications (settings.notifications)
[ ] Config (auto-refresh) (settings.config)
[ ] Security info (settings.security)
[ ] Version info (settings.version)
[ β–Ά πŸ“ Knowledge Base ] ← parent toggle, OFF by default (future)
```
**Sub-permission toggle rules:**
- **Auto-ON children:** `sessions.view`, `analytics.dashboard`, and `whatsapp.view` auto-enable when their parent is toggled ON, since they are the minimum required to use that section at all.
- **Parent controls children:** Turning a parent toggle OFF **collapses** the sub-group and **clears** all child selections instantly.
- **"Select All" / "Clear All"** shortcut appears at the top of each expanded sub-group.
- A count badge shows how many sub-permissions are enabled per group: e.g. **"Live Sessions (3/7)"**.
---
#### D2. Edit Admin Modal
Same modal reused in edit mode. Differences from create mode:
| Field | Behaviour in Edit Mode |
|---|---|
| Username | **Read-only** β€” displayed but not editable |
| Email | Editable |
| Password | **Hidden** β€” replaced by a "Reset Password" button that opens a separate sub-modal |
| Role | Editable. Changing to Superadmin shows warning banner (see Rule 25). |
| Permissions | Pre-filled with the admin's current permissions. Fully editable. |
**Dirty-state detection:**
- The "Save Changes" button starts **grayed out** on modal open.
- It activates only when at least one field or toggle has been changed from its original value.
- If the modal is closed without saving and there are unsaved changes, a browser-style confirmation appears: **"Discard unsaved changes?"** with `[Discard]` and `[Keep Editing]` buttons.
**Superadmin upgrade warning:**
- When a superadmin changes a sub-admin's role to `Superadmin`, a yellow warning banner appears inside the modal:
> ⚠️ You are about to grant **full unrestricted access**. This sub-admin will be able to manage all other admins and all system settings. Click Save again to confirm.
- The Save button text changes to **"Confirm & Save"** and requires one more click.
**Reset Password sub-modal (inside edit):**
- Triggered by "Reset Password" button inside the edit modal.
- Fields: New Password, Confirm New Password.
- Only available to superadmins editing another user. A sub-admin editing themselves uses the dedicated "Change Password" modal (Section E).
- On success: shows toast "Password reset for `{username}`."
- The affected sub-admin's next auto-login re-validation will fail and force them to log in again with the new password.
---
#### D3. Permission Badges in Table
The admin users table shows permissions as **colored pill badges** per user:
| Permission | Badge Color | Label |
|---|---|---|
| `manage_sessions` | Blue | Sessions |
| `view_analytics` | Purple | Analytics |
| `manage_whatsapp` | Green | WhatsApp |
| `manage_kb` | Orange | KB |
- Superadmin rows show a single gold **"Full Access"** badge instead of individual permission pills.
- If a sub-admin has **no permissions** (should not happen, but defensive), their row shows a red **"No Access"** badge.
> [!NOTE]
> Permission toggles (during both create and edit) are **disabled** for Superadmin role β€” all permissions are implied and cannot be individually scoped.
#### E. Change Password Modal
- A "Change Password" button added to the **sidebar footer** (next to "Sign Out"), visible to all logged-in users.
- Modal fields: Current Password, New Password, Confirm New Password.
- Client-side validation: new and confirm must match; length >= 8.
- On success: show toast "Password changed. Please log in again." β†’ force logout after 2 seconds (credentials are now invalid).
- On failure (wrong current password): show error inline.
#### F. Sidebar Profile Chip
- Add a small profile chip in the **sidebar footer area** above "Sign Out" showing:
- Avatar initial (first letter of username)
- Username
- Role badge (small, colored)
- This replaces the raw button look with a more polished identity indicator.
#### G. Auto-login Re-validation
- Current code: reads `localStorage` and auto-logs in if `otp_verified: true`. In multi-admin world, this still works for HTTP Basic Auth.
- **Edge case:** After auto-login, we must still call `GET /api/admin/me` with the stored credentials. If it returns `401` (password changed, user deleted, account locked), **force logout** and redirect to login screen with message "Your session has expired."
- This prevents ghost sessions where a deleted or demoted admin stays logged in indefinitely.
---
## Admin Ease Features
These are quality-of-life improvements that make daily admin management faster and less error-prone. All are purely additive β€” they do not affect existing auth or permission logic.
---
### Ease Feature 1 β€” Suspend / Activate Toggle
**What it does:** A superadmin can temporarily disable a sub-admin account **without deleting it**. The account stays in `admins.json` with all its permissions intact, but the user cannot log in.
**Rules:**
- A suspended admin's `is_active` field is set to `false`.
- Attempting to log in as a suspended admin returns `403` (not `401`) with the message: "Your account has been suspended. Contact your superadmin."
- Suspending does **not** affect the last superadmin (superadmins cannot be suspended while they are the only one).
- A superadmin **cannot suspend their own account**.
- Suspension is reflected immediately β€” no restart required.
**UI:**
- In the admin users table, each row has a **Status toggle switch** (green = Active, gray = Suspended) next to the action buttons.
- Toggling it shows a confirmation: "Suspend `{username}`? They will be locked out immediately." or "Reactivate `{username}`?"
- Suspended rows are styled with a **dimmed / strikethrough** appearance in the table.
- A red **"Suspended"** badge replaces the role badge in the table row.
**Backend:**
- `PATCH /api/admin/users/{username}/status` β†’ `{ "is_active": true/false }` β€” requires `admins.edit`.
- `verify_password()` checks `is_active` before proceeding.
---
### Ease Feature 2 β€” Permission Templates
**What it does:** Pre-defined permission sets that a superadmin can apply to a new or existing sub-admin with one click, instead of manually toggling every permission.
**Built-in templates:**
| Template Name | Permissions Included |
|---|---|
| 🎧 Support Agent | `manage_sessions`, `sessions.view`, `sessions.reply`, `sessions.takeover` |
| πŸ‘οΈ Read-Only Viewer | `manage_sessions`, `sessions.view`, `analytics.dashboard`, `analytics.sla` |
| πŸ“Š Analytics Manager | `view_analytics`, `analytics.dashboard`, `analytics.sla`, `analytics.export` |
| πŸ“± WhatsApp Manager | `manage_whatsapp`, `whatsapp.view`, `whatsapp.edit`, `whatsapp.test`, `whatsapp.qr` |
| πŸ›‘οΈ Full Sub-Admin | All non-admin permissions (everything except `manage_admins`) |
**Rules:**
- Templates are **suggestions only** β€” after applying a template, all toggles remain individually editable.
- Applying a template **overwrites** any currently selected permissions in the modal (with a confirmation: "Apply template? This will replace your current selection.").
- Custom templates can be saved by the superadmin (stored in `admins.json` under a `"templates"` key).
- A superadmin cannot save or apply a template that contains permissions they don't have themselves.
**UI:**
- A **"Use Template" dropdown button** appears above the permission toggle group in both Create and Edit modals.
- After selecting a template, the toggles animate to their new states.
- A **"Save as Template"** button appears when the current permission set doesn't match any existing template.
---
### Ease Feature 3 β€” Clone Permissions from Existing Admin
**What it does:** When creating a new sub-admin, a superadmin can copy the exact permission set of an existing admin as the starting point.
**Rules:**
- Cloning is only available during **create mode** (not edit β€” use templates for edit mode).
- A superadmin can only clone from admins whose permissions are a **subset of their own**.
- Cloning from a superadmin account produces a sub-admin with all non-admin permissions (cannot clone superadmin status).
- After cloning, all toggles are editable individually.
**UI:**
- A **"Copy from existing admin"** link appears below the "Use Template" button.
- Opens a small dropdown/search of existing admin usernames.
- On selection, the permission toggles animate to match the cloned admin's set.
- A note appears: "Permissions copied from `{username}`. You can adjust below."
---
### Ease Feature 4 β€” Audit Log
**What it does:** A chronological log of all admin management actions taken by any superadmin. This gives a paper trail for accountability.
**Logged actions:**
- Admin created / deleted / suspended / reactivated
- Permissions changed (stores before + after)
- Password reset by superadmin
- Role changed (promoted / demoted)
- Force logout triggered
**Rules:**
- Audit log entries are **append-only** β€” they can never be deleted via the UI.
- Each entry contains: `timestamp`, `actor` (who did it), `action` (what), `target` (who it was done to), `detail` (human-readable summary).
- The log is stored in `admins.json` under an `"audit_log"` array.
- The log is capped at **500 entries** β€” oldest entries are pruned automatically when the cap is exceeded.
- The log is accessible only to users with `manage_admins` permission.
**UI:**
- An **"Audit Log"** tab/button inside the Admin Users view (beside the "New Admin" button).
- Renders as a timeline list: avatar icon of the actor, action description, target, and relative timestamp ("2 hours ago").
- Filterable by: actor, action type, date range.
- The 10 most recent entries are shown inline; older entries load on scroll (pagination).
**Backend:**
- `GET /api/admin/audit-log?limit=50&offset=0` β€” requires `admins.view`.
---
### Ease Feature 5 β€” Admin Notes / Description
**What it does:** An optional free-text notes field on each admin account. Useful for recording context like "HR department, Pakistan office" or "Temp account, expires July 2026".
**Rules:**
- Notes are plain text, max **500 characters**.
- Notes are visible to all users with `admins.view`.
- Notes can be edited by users with `admins.edit`.
- Notes are shown in the admin table as a tooltip on hover (truncated to 60 chars inline).
**UI:**
- A **Notes** textarea field in both Create and Edit modals, below the email field.
- In the admin users table, rows with non-empty notes show a small **πŸ“ icon** that reveals the full note on hover.
---
### Ease Feature 6 β€” Force Logout
**What it does:** A superadmin can immediately invalidate a specific sub-admin's active session, forcing them to log in again on their next API call.
**How it works technically:**
- A `force_logout_at` timestamp is stored per admin in `admins.json`.
- Every authenticated API call checks if `force_logout_at` is newer than when the user last logged in (`last_login`).
- If `force_logout_at > last_login`, the request returns `401` with message "Your session has been terminated by an administrator."
- The sub-admin's `localStorage` credentials are then cleared on the frontend and they are redirected to the login screen.
**Rules:**
- A superadmin **cannot force-logout themselves**.
- Force logout does **not** delete the account or change the password.
- After the sub-admin logs in again, `force_logout_at` has no further effect.
**UI:**
- A **"Force Logout"** button in the edit modal (appears next to "Reset Password").
- Confirmation prompt: "Force `{username}` to log out immediately?"
- After confirmation: shows toast "Session terminated for `{username}`."
**Backend:**
- `POST /api/admin/users/{username}/force-logout` β€” requires `admins.edit`.
- The `GET /api/admin/me` endpoint checks `force_logout_at` and returns `401` if triggered.
---
### Ease Feature 7 β€” Welcome Email on Account Creation
**What it does:** When a new sub-admin account is created, they automatically receive a welcome email with their username and a prompt to log in and change their temporary password.
**Rules:**
- The welcome email is sent using the same email service already used for OTPs.
- The email contains: username, a note that their password was set by the superadmin, and a link to the admin panel login page.
- The email does **not** contain the password (for security). Only the username.
- If the sub-admin has no email configured, no email is sent (silently skipped β€” same as OTP fallback).
- The superadmin creating the account sees a toast: "Account created. Welcome email sent to `{email}`."
**UI:**
- No extra UI required β€” this happens automatically in the background on `POST /api/admin/users`.
- An optional **"Resend Welcome Email"** button in the edit modal (only shown if the sub-admin has never logged in β€” `last_login` is `null`).
**Backend:**
- Triggered automatically inside the `create_admin()` flow in `admin_store.py`.
---
### Ease Feature 8 β€” Search & Filter in Admin Table
**What it does:** Makes it easy to find a specific admin in the table when there are many accounts.
**Filter options:**
- **Search box** β€” real-time filter by username or display name
- **Role filter** β€” "All", "Superadmin", "Sub-admin"
- **Status filter** β€” "All", "Active", "Suspended"
- **Permission filter** β€” dropdown to show only admins who have a specific permission (e.g., show only users with `manage_sessions`)
**Rules:**
- Filtering is **client-side only** (no extra backend calls) β€” the full admin list is already loaded.
- Filters are combined with AND logic (e.g., Active + has Sessions permission).
- The count of shown results is displayed: "Showing 3 of 7 admins".
- Clearing all filters restores the full list.
**UI:**
- A filter bar above the admin table with the search box on the left and filter dropdowns on the right.
- An **"Active filters"** chip row showing applied filters with `Γ—` to remove individual ones.
---
### Ease Feature 9 β€” Credential Card (Copy Credentials for New Admin)
**What it does:** After creating a new admin, show a one-time credential summary card that the superadmin can copy and send to the new user manually (e.g., via WhatsApp or email).
**Content of the card:**
```
Admin Panel Login Details
─────────────────────────
URL: https://yoursite.com/admin
Username: hr_user
Password: [the temporary password set during creation]
─────────────────────────
Please log in and change your password immediately.
```
**Rules:**
- The credential card is shown **exactly once** β€” immediately after the create modal is submitted successfully.
- It is never stored anywhere. Once dismissed, it cannot be retrieved.
- A **"Copy to Clipboard"** button copies the full card text.
- A clear warning is shown: "⚠️ This is the only time you will see this password. Save it now."
- The password is shown as plaintext in the card (it is the temp password the superadmin just typed in).
**UI:**
- Appears as a modal overlay immediately after the create success toast.
- Styled as a receipt/card with a monospace font and a prominent copy button.
- Dismissing the card requires a deliberate click: **"I have saved the credentials"** button.
---
### Ease Feature 10 β€” Password Age Warning
**What it does:** Warns a sub-admin (and the superadmin viewing their profile) if their password has not been changed in over 90 days.
**Rules:**
- The `password_changed_at` field tracks when the password was last changed (updated on `change_password` and `reset_password`).
- If `password_changed_at` is older than 90 days, the sub-admin sees a **banner** on login: "Your password is 95 days old. Consider changing it." (dismissable per session).
- Superadmins viewing a sub-admin's row in the table see a **πŸ”” yellow badge** on that row indicating stale password.
- This is a **warning only** β€” logins are not blocked. No forced password expiry.
**UI:**
- Banner shown in the topbar area after login (dismissable, does not reappear until next login session).
- In the admin table, a `password_changed_at` column with a relative date ("43 days ago") and a yellow clock icon if > 90 days.
---
### New Backend Endpoints for Ease Features
```
PATCH /api/admin/users/{username}/status β†’ Suspend/activate [admins.edit]
POST /api/admin/users/{username}/force-logout β†’ Force logout [admins.edit]
GET /api/admin/audit-log β†’ Get audit log [admins.view]
POST /api/admin/users/{username}/resend-welcome β†’ Resend welcome email [admins.edit]
```
---
### New Rules for Ease Features
33. A suspended admin (`is_active: false`) cannot log in. Login attempt returns `403 Suspended`.
34. The last active superadmin cannot be suspended.
35. A superadmin cannot suspend their own account.
36. Force logout is checked on every authenticated API call by comparing `force_logout_at` against `last_login`.
37. The credential card is shown once immediately after admin creation and is never retrievable again.
38. Audit log entries are append-only and capped at 500; oldest entries are pruned automatically.
39. Permission templates cannot contain permissions the applying superadmin doesn't have themselves.
40. "Clone permissions" only clones permissions that are a subset of the superadmin's own permissions.
41. The password age warning fires at 90 days and is advisory only β€” it does not block logins.
42. Welcome emails are sent automatically on creation and can be resent only if `last_login` is `null`.
---
## Data Flow Diagrams
### Login Flow (Updated)
```
[User enters username + password]
↓
POST /api/admin/request-otp
↓
[Backend: check locked β†’ verify password via admin_store β†’ generate OTP β†’ send to user's own email]
↓ (on success)
[Frontend: show OTP input]
↓
POST /api/admin/verify-otp
↓
[Backend: verify password again + OTP code β†’ record_login β†’ clear OTP]
↓ (on success)
[Frontend: store creds in localStorage β†’ call GET /api/admin/me β†’ applyPermissions() β†’ show UI]
```
### Permission Enforcement Flow (Backend)
```
[API Request arrives with HTTP Basic Auth]
↓
verify_admin() β†’ admin_store.verify_password()
↓ (401 if wrong creds, 423 if locked)
require_permission("manage_sessions")() β†’ admin_store.get_admin(username).permissions
↓ (403 if permission missing)
[Route handler executes]
```
---
## Migration Strategy
At first startup after deployment:
1. `seed_initial_admin()` runs.
2. It checks if `data/admins.json` exists and has at least one entry.
3. If not: creates the file with a single superadmin seeded from `ADMIN_USER` / `ADMIN_PASS` / `ADMIN_EMAIL` in `.env`. The password is hashed using `pbkdf2_hmac` at this point.
4. The `.env` `admin_user` / `admin_pass` settings remain in place as an **emergency fallback** in `verify_admin`. If the `admins.json` file is ever corrupted or deleted, the `.env` credentials will still grant access (and `seed_initial_admin` will recreate the file on next restart).
---
## Verification Plan
### Manual Test Checklist
**Authentication & Security**
| # | Scenario | Expected Result |
|---|---|---|
| 1 | Fresh deploy (no `admins.json`) β€” log in with `.env` creds | βœ… Seeds file, login works |
| 2 | Enter wrong password 5 times | βœ… Account locked 15 min, returns 423 |
| 3 | Enter wrong OTP 5 times | βœ… OTP invalidated, must request new one |
| 4 | HR receives OTP to their own email (not global admin email) | βœ… Email matches HR's configured email |
| 5 | After HR changes password, auto-login re-validates | βœ… Old session invalidated, shown login screen |
| 6 | Superadmin resets HR's password β€” HR auto-session expires | βœ… HR forced to log in with new password |
**Backend Sub-Permission Enforcement**
| # | Scenario | Expected Result |
|---|---|---|
| 7 | HR (sessions.view only) calls `POST /sessions/{id}/reply` | βœ… Returns 403 |
| 8 | HR (sessions.view only) calls `POST /sessions/{id}/takeover` | βœ… Returns 403 |
| 9 | HR (sessions.view + sessions.reply) calls `POST /sessions/{id}/reply` | βœ… Returns 200 |
| 10 | HR (analytics.dashboard only) calls `GET /api/admin/sla-metrics` | βœ… Returns 403 |
| 11 | HR (analytics.dashboard + analytics.sla) calls `GET /api/admin/sla-metrics` | βœ… Returns 200 |
| 12 | HR (whatsapp.view only) calls `POST /api/admin/settings/whatsapp` | βœ… Returns 403 |
| 13 | HR (whatsapp.view only) calls `GET /api/admin/settings/whatsapp` | βœ… Returns 200 |
| 14 | HR calls `GET /api/admin/export/all` without `analytics.export` | βœ… Returns 403 |
| 15 | Try to delete the only superadmin | βœ… Returns 400 |
| 16 | Try to create admin with duplicate username | βœ… Returns 409 |
| 17 | Try to create sub-admin with zero permissions via API | βœ… Returns 400 |
| 18 | Try to create sub-admin with `sessions.reply` but no `sessions.view` | βœ… Returns 400 (invalid combo) |
**Sidebar Visibility (Level 1)**
| # | Scenario | Expected Result |
|---|---|---|
| 19 | HR (sessions only) logs in β€” check sidebar | βœ… Only Sessions nav items visible; Dashboard, SLA, Settings, Admin Users all hidden |
| 20 | HR (analytics only) logs in β€” check sidebar | βœ… Only Dashboard and SLA items visible |
| 21 | HR (whatsapp.view only) logs in β€” check sidebar | βœ… Only Settings visible; no other nav items |
| 22 | HR with no permissions logs in | βœ… Sidebar empty; full-screen "no access" state shown |
| 23 | Superadmin logs in β€” check sidebar | βœ… All 10 nav items visible |
**In-View Sub-Element Visibility (Level 2)**
| # | Scenario | Expected Result |
|---|---|---|
| 24 | HR has `sessions.view` but NOT `sessions.reply` | βœ… Reply input area is hidden; transcript is read-only |
| 25 | HR has `sessions.view` but NOT `sessions.block` | βœ… "Block" button in chat header and context menu are hidden |
| 26 | HR has `sessions.view` but NOT `sessions.delete` | βœ… "Delete" in context menu and delete buttons in header are hidden |
| 27 | HR has `sessions.view` but NOT `sessions.takeover` | βœ… "Take Over" button hidden |
| 28 | HR has `analytics.dashboard` but NOT `analytics.export` | βœ… "Export Data" topbar button hidden; export button in chat header hidden |
| 29 | HR has `analytics.dashboard` but NOT `analytics.sla` | βœ… SLA nav item hidden; SLA view inaccessible |
| 30 | HR has `whatsapp.view` but NOT `whatsapp.edit` | βœ… Save Settings button hidden; all inputs are read-only |
| 31 | HR has `whatsapp.view` but NOT `whatsapp.test` | βœ… "Send Test Message" button hidden |
| 32 | HR has `whatsapp.view` but NOT `whatsapp.qr` | βœ… QR code fetch button hidden |
| 33 | HR has settings access but only `settings.notifications` | βœ… Only Notifications sub-section visible in Settings; WhatsApp, Config, Security, Version all hidden |
| 34 | HR has `admins.view` but NOT `admins.create` | βœ… "New Admin" button hidden; table is read-only |
| 35 | HR has `admins.edit` but NOT `admins.reset_password` | βœ… Edit modal opens but "Reset Password" button is absent |
**Permission Toggle UI β€” Create Modal (with sub-permissions)**
| # | Scenario | Expected Result |
|---|---|---|
| 36 | Open "New Admin" modal β€” all parent toggles | βœ… All 4 parent toggles OFF, sub-groups collapsed |
| 37 | Enable "Live Sessions" parent toggle | βœ… Sub-group expands; `sessions.view` auto-checks ON; others OFF |
| 38 | Disable "Live Sessions" parent toggle | βœ… Sub-group collapses; all sub-permissions cleared |
| 39 | Enable parent, click "Select All" in sub-group | βœ… All 7 sessions sub-permissions toggle ON |
| 40 | Count badge on parent toggle | βœ… Shows "Live Sessions (1/7)" with only view enabled |
| 41 | Switch to Superadmin role | βœ… All parent toggles and sub-groups hidden; "Full Access" badge shown |
| 42 | Create HR with `sessions.view` + `sessions.reply` only | βœ… `admins.json` contains `["manage_sessions","sessions.view","sessions.reply"]` |
**Permission Toggle UI β€” Edit Modal**
| # | Scenario | Expected Result |
|---|---|---|
| 43 | Open edit for HR with `sessions.view` + `sessions.reply` | βœ… Sessions parent ON; sub-group expanded; view+reply checked; others unchecked |
| 44 | Uncheck `sessions.reply`, close without saving | βœ… Discard confirmation shown |
| 45 | Turn all sub-permissions OFF but leave parent ON | βœ… Validation error: parent must have at least one sub-permission |
| 46 | Superadmin edits HR β€” HR currently active | βœ… Change takes effect on HR's next API call |
**Suspend / Activate (Ease Feature 1)**
| # | Scenario | Expected Result |
|---|---|---|
| 47 | Suspend HR account via table toggle | βœ… `is_active: false` saved; HR's table row dims with "Suspended" badge |
| 48 | HR tries to log in while suspended | βœ… Returns 403 "Account suspended" |
| 49 | Reactivate HR from table | βœ… HR can log in again |
| 50 | Try to suspend the only superadmin | βœ… Disabled; confirmation blocked with error |
| 51 | Superadmin tries to suspend themselves | βœ… Blocked; error shown |
**Permission Templates (Ease Feature 2)**
| # | Scenario | Expected Result |
|---|---|---|
| 52 | Apply "Support Agent" template in create modal | βœ… Toggles animate to `sessions.view + sessions.reply + sessions.takeover` ON |
| 53 | Apply template with existing selections | βœ… Confirmation prompt appears before overwriting |
| 54 | Save current toggle state as custom template | βœ… Template saved; appears in "Use Template" dropdown |
**Audit Log (Ease Feature 4)**
| # | Scenario | Expected Result |
|---|---|---|
| 55 | Create a new admin β€” check audit log | βœ… New entry: actor, "create_admin", target, permission detail |
| 56 | Change HR's permissions β€” check audit log | βœ… Before + after permissions recorded |
| 57 | Sub-admin (non-manage_admins) calls `GET /api/admin/audit-log` | βœ… Returns 403 |
**Force Logout (Ease Feature 6)**
| # | Scenario | Expected Result |
|---|---|---|
| 58 | Force logout HR from edit modal | βœ… `force_logout_at` updated; next API call by HR returns 401 |
| 59 | HR receives 401 after force logout | βœ… Frontend clears session, shows login screen with "terminated" message |
| 60 | HR logs back in after force logout | βœ… Session restored; `force_logout_at` no longer blocks |
**Credential Card (Ease Feature 9)**
| # | Scenario | Expected Result |
|---|---|---|
| 61 | Create new admin β€” credential card behavior | βœ… Card modal appears immediately after creation with username + password |
| 62 | Click "Copy to Clipboard" on card | βœ… Full card text copied; toast "Copied!" shown |
| 63 | Close card, reopen admin edit modal | βœ… Password is NOT visible anywhere in edit modal (not recoverable) |