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)

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.

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.

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)

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

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

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

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

  1. Sub-permissions are nested under their parent. A sub-admin cannot have sessions.reply without also having sessions.view β€” the backend rejects such combinations.
  2. 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.
  3. 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).
  4. Each sub-permission group has a "Select All" / "Clear All" shortcut link next to the group header for quick bulk-toggling.
  5. 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.
  6. Superadmins have all sub-permissions implicitly. Their permissions field in admins.json stores only the parent-level keys; sub-permissions for superadmins are implied.
  7. 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

  1. 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).
  2. A sub-admin cannot grant themselves permissions they do not have.
  3. A sub-admin cannot create, edit, or delete other admins unless they have manage_admins.
  4. A sub-admin cannot change another user's password β€” only their own.
  5. A sub-admin's email must be unique in the store. Duplicate emails are rejected.

Permission Toggle Rules (During Creation)

  1. When creating a sub-admin, all permission toggles default to OFF β€” no permissions are granted unless explicitly turned on by the superadmin.
  2. 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.
  3. 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."
  4. 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)

  1. 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.
  2. Toggling a permission in the edit modal does not auto-save β€” the superadmin must click "Save Changes" to commit.
  3. 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."
  4. 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).
  5. 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.
  6. The edit modal shows a read-only "last modified" timestamp so the superadmin knows when permissions were last changed.

Permission Toggle β€” UI Behaviour Rules

  1. Each toggle has a label (the feature name) and a description (one line explaining what access it grants). This removes ambiguity.
  2. Toggling a permission in the UI gives an instant visual response β€” the toggle animates to the new state immediately, before saving.
  3. 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).
  4. If the superadmin closes the edit modal without saving, a discard confirmation is shown: "You have unsaved changes. Discard them?" β€” prevents accidental permission loss.
  5. 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

  1. Passwords must be at least 8 characters.
  2. When changing password, the current password must be verified before the new password is set.
  3. Passwords are stored as pbkdf2_hmac hash+salt β€” never in plaintext.
  4. 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

  1. OTP is per-user, sent to their own email (not the global settings.admin_email).
  2. If an admin has no email configured, the OTP is only logged to the server console (existing fallback behavior).
  3. OTP codes expire in 5 minutes (unchanged).
  4. After 5 failed OTP attempts, the OTP is invalidated and a new one must be requested.
  5. OTP is single-use β€” verified once and immediately deleted from the in-memory store.

Brute-Force / Security Rules

  1. 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).
  2. All credential comparisons use secrets.compare_digest() to prevent timing attacks.
  3. Admin usernames are case-insensitive for lookup but stored in their original casing.
  4. 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

  1. Admin data is stored in a data/admins.json file (same persistent volume as sessions).
  2. File writes use an atomic write pattern (write to .tmp, then os.replace()). This prevents data corruption if the server crashes mid-write.
  3. All reads/writes to admins.json are wrapped in an asyncio.Lock to prevent race conditions.
  4. 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):

{
  "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"
    }
  ]
}

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.

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

  1. A suspended admin (is_active: false) cannot log in. Login attempt returns 403 Suspended.
  2. The last active superadmin cannot be suspended.
  3. A superadmin cannot suspend their own account.
  4. Force logout is checked on every authenticated API call by comparing force_logout_at against last_login.
  5. The credential card is shown once immediately after admin creation and is never retrievable again.
  6. Audit log entries are append-only and capped at 500; oldest entries are pruned automatically.
  7. Permission templates cannot contain permissions the applying superadmin doesn't have themselves.
  8. "Clone permissions" only clones permissions that are a subset of the superadmin's own permissions.
  9. The password age warning fires at 90 days and is advisory only β€” it does not block logins.
  10. 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)