| # 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) | |
|
|