Skip to content

PyroRadius — User Roles & Access Control

Version: 1.0 (Documentation) Last Updated: 2026-07-13


Table of Contents

  1. Role Hierarchy Overview
  2. Super Admin
  3. Admin
  4. Dealer
  5. Sub Dealer
  6. Customer Portal User
  7. Field Engineer
  8. Dealer Hierarchy Deep Dive
  9. View As Feature
  10. Permission System Architecture
  11. Permission Keys Reference
  12. Customer Portal Details

Role Hierarchy Overview

┌─────────────────────────────────────────────────────────────────────┐
│                         SUPER ADMIN                                 │
│  Full access to everything. System settings, all dealers,           │
│  all customers, all financial data, RADIUS config.                  │
│  Role string: 'super_admin'                                         │
└───────────────────────────────┬─────────────────────────────────────┘
                                │ manages
┌───────────────────────────────▼─────────────────────────────────────┐
│                            ADMIN                                    │
│  Operational staff. Manages dealers, packages, NAS devices.         │
│  Can "View As" any dealer. Cannot access system-level settings.     │
│  Role string: 'admin'                                               │
└───────────────────────────────┬─────────────────────────────────────┘
                                │ manages
┌───────────────────────────────▼─────────────────────────────────────┐
│                           DEALER                                    │
│  Independent reseller. Sees only their own customers.               │
│  Permissions explicitly granted as JSONB keys.                      │
│  Role string: 'dealer'                                              │
└───────────────────────────────┬─────────────────────────────────────┘
                                │ manages
┌───────────────────────────────▼─────────────────────────────────────┐
│                         SUB DEALER                                  │
│  Under a dealer. Restricted subset of dealer permissions.           │
│  Cannot see other sub-dealers' customers.                           │
│  Role string: 'sub_dealer'                                          │
└─────────────────────────────────────────────────────────────────────┘

Separate authentication system:
┌─────────────────────────────────────────────────────────────────────┐
│                    CUSTOMER PORTAL USER                             │
│  End customer. Logs in at /portal. Sees only their own account.     │
│  No role string — identified by customer_id in portal session.      │
└─────────────────────────────────────────────────────────────────────┘

Super Admin

Role string: super_admin Defined in: users.role column Permission bypass: Yes — super_admin bypasses all JSONB permission checks

What Super Admin Can See and Do

Area Access
All customers (all dealers) Full CRUD
All dealers and sub-dealers Full CRUD
All invoices and payments (system-wide) Full CRUD
All ledger entries Read + adjustment
Package management Full CRUD
NAS device management Full CRUD + API credential management
OLT management Full CRUD
WhatsApp templates Full CRUD
Campaign management Full CRUD
System settings (key-value store) Read + Write
Company profile Read + Write
Audit logs Read + Prune
Cron job scheduler Full CRUD + manual trigger
RADIUS direct management Full access
TR-069 / GenieACS Full access
Report export (all data) Full access
Bulk customer import Full access
User management (all roles) Full CRUD
View As any dealer Yes
Firewall / website bypass Full CRUD
Engineer team management Full CRUD

What Super Admin Cannot Do (by policy, not technical)

  • Directly execute shell commands (no such feature in the UI)
  • Delete audit_logs records individually (only bulk pruning via command)
  • Permanently delete paid invoices (soft delete or archive only)

Admin

Role string: admin Defined in: users.role column Permission bypass: Yes — admin bypasses all JSONB permission checks

What Admin Can See and Do

Admins have near-identical access to super_admin with these distinctions:

Area Access
All customers (all dealers) Full CRUD
Dealers Create + Edit (cannot delete dealers with customers)
System settings Read only (some keys)
Company profile Read only
Billing reports (system-wide) Yes
View As any dealer Yes
Package management Full CRUD
NAS management Full CRUD
OLT management Full CRUD
Audit logs Read
Cron jobs Full management
User management Create/Edit dealers and sub-dealers

What Admin Cannot Do

  • Access super_admin-only settings (if any are gated at that level)
  • Manage other admin accounts (only super_admin manages admins)

Dealer

Role string: dealer Defined in: users.role column Permission bypass: No — all access controlled by JSONB permissions column Scoping: All queries automatically filtered by dealer_id = auth()->id()

What Dealer Can See (Always)

Even a dealer with minimal permissions always sees: - Their own profile - Their own financial summary (what they owe PyroNet) - Customers assigned to them (list view, basic info) - Their own sub-dealers

What Dealer Can Do (Depends on Granted Permissions)

The following capabilities are controlled by explicit permission keys granted by an admin:

Permission Key What It Enables
customers.view View full customer details (not just list)
customers.create Add new customers
customers.edit Edit customer details, change package, change status
customers.delete Soft-delete customers
billing.invoices View invoices
billing.payments Record payments
billing.recharge Renew customer subscriptions
reports.view Access reports module
whatsapp.send Manually send WhatsApp messages
tickets.view View support tickets
tickets.create Create tickets
tickets.manage Assign and resolve tickets

What Dealer Cannot Do (Regardless of Permissions)

  • See other dealers' customers (hard database scoping)
  • Access system settings
  • Manage packages or NAS devices
  • View audit logs of other dealers
  • Access RADIUS configuration
  • Use the "View As" feature
  • Add packages (they sell only pre-assigned packages via dealer_package)

Sub Dealer

Role string: sub_dealer Defined in: users.role column Permission bypass: No Scoping: Filtered by both dealer_id (parent dealer) AND sub_dealer_id (own ID)

What Sub Dealer Can See

A sub-dealer sees only: - Customers assigned to them specifically (customers.sub_dealer_id = auth()->id()) - Their own profile - Their own financial summary within the dealer's account

What Sub Dealer Can Do

Sub-dealers receive a subset of whatever permissions their parent dealer has been granted. An admin cannot grant a sub-dealer more permissions than the parent dealer has.

Typical sub-dealer permissions: - Record payments for their own customers - View customer status and expiry - View invoices for their customers

Dealer-to-Sub-Dealer Relationship

users table:
  id=10, role='dealer',     dealer_id=NULL,  name='Dealer ABC'
  id=20, role='sub_dealer', dealer_id=10,    name='Sub Dealer XYZ'

customers table:
  customer_id=100, dealer_id=10, sub_dealer_id=20, name='Customer 1'
  customer_id=101, dealer_id=10, sub_dealer_id=NULL, name='Customer 2' (dealer's own)
  customer_id=102, dealer_id=10, sub_dealer_id=20, name='Customer 3'

Sub Dealer XYZ (id=20) sees: customers 100, 102 (where sub_dealer_id=20)
Dealer ABC (id=10) sees: customers 100, 101, 102 (all where dealer_id=10)

Customer Portal User

Authentication: Separate Laravel guard — portal guard Route prefix: /portal Sessions: Separate session namespace from admin sessions

Customer portal users are end-customers of Indus Broadband. They do not have a role in the users table; instead, they authenticate against the customers table (or a separate portal credentials field). Their session contains only their customer_id.

What Customer Portal Users Can Do

Feature Access
View own account details Yes (name, package, expiry, connection status)
View own invoice history Yes (download PDF)
View current balance Yes
View payment history Yes
Create support ticket Yes
View own ticket status Yes
View own traffic usage (if enabled) Yes
Change own portal password Yes

What Customer Portal Users Cannot Do

  • See any other customer's data
  • Modify their own package, expiry, or connection settings
  • Record payments (contact dealer or sub-dealer)
  • Access any admin routes

Portal Route Separation

All portal routes are in routes/portal.php and use: - Separate middleware group: portal (not web) - Separate auth guard: portal - Separate login controller: Auth\PortalLoginController - Separate session: portal users never share auth tokens with admin users


Field Engineer

Field engineers are managed via the EngineerTeam model and the Engineer module in the admin UI. They may or may not have user accounts in the users table. The engineer module tracks:

  • Engineer team members and their assignments
  • Ticket assignments to specific engineers
  • Installation/fault job tracking

If engineers have user accounts, their role would be a restricted admin variant or a custom role depending on implementation. The engineer_teams table links engineers to teams, and tickets can be assigned to engineers by admin/dealer staff.

Engineers cannot: - Access billing or financial data - Create/delete customers - Manage RADIUS or network configuration


Dealer Hierarchy Deep Dive

How Dealers Are Created

  1. Super Admin or Admin creates a dealer user account (role = dealer)
  2. Admin assigns permissions to the dealer (JSONB keys in users.permissions)
  3. Admin assigns packages to the dealer via dealer_package pivot (with per-dealer pricing)
  4. Dealer can then log in and start creating customers

How Sub-Dealers Are Created

  1. A dealer (or admin) creates a sub-dealer user account under the dealer's account
  2. users.dealer_id is set to the parent dealer's users.id
  3. users.role = sub_dealer
  4. Permissions assigned as a subset of the dealer's permissions

Customer Ownership

A customer belongs to exactly one dealer and optionally one sub-dealer: - customers.dealer_id = the dealer (always set) - customers.sub_dealer_id = the sub-dealer (nullable)

When a sub-dealer creates a customer, both dealer_id and sub_dealer_id are set. When a dealer creates a customer directly, only dealer_id is set.

Financial Chain

Customer pays → recorded as payment with dealer_id
Dealer owes PyroNet for activated customers
PyroNet (super_admin/admin) views dealer-level financial summaries
Reports show: dealer collection vs. dealer liability to PyroNet

The dealer_package.price column defines what PyroNet charges the dealer per customer per month for a given package. The dealer collects their own price from customers (possibly higher) and keeps the margin.


View As Feature

The "View As" feature allows super_admin and admin users to impersonate the view of any dealer without logging out. This is used to: - Diagnose dealer-reported issues - Verify that dealer scoping is working correctly - Audit what a specific dealer can see

How It Works

  1. Admin selects a dealer from a dropdown (typically in the admin navigation or a dedicated "View As" modal)
  2. The selected dealer ID is stored in the admin's session (e.g., session(['viewing_as_dealer' => $dealerId]))
  3. A middleware or controller base class checks for this session key
  4. If set, all queries are scoped as if the authenticated user were that dealer
  5. A banner is shown in the UI: "Viewing as: Dealer Name [Exit View As]"
  6. Admin clicks "Exit View As" to clear the session key and return to their own view

Security Notes

  • "View As" does not give admin the dealer's JSONB permissions — the admin retains their own bypass
  • All actions taken while in "View As" mode are logged under the admin's own user_id in audit_logs, with a note of the dealer being viewed as
  • "View As" is not available to dealers themselves

Permission System Architecture

Storage

Permissions are stored in the users.permissions JSONB column. Each key represents a capability:

{
    "customers.view": true,
    "customers.create": true,
    "customers.edit": true,
    "customers.delete": false,
    "billing.invoices": true,
    "billing.payments": true,
    "billing.recharge": false,
    "reports.view": true,
    "whatsapp.send": false,
    "tickets.view": true,
    "tickets.create": true,
    "tickets.manage": false
}

Only explicitly set keys matter. A missing key is treated the same as false.

The User::can() Method

Laravel's Gate / Policy system is used, but PyroRadius also has a custom can() check on the User model for JSONB-based permissions:

// In the User model or a trait:
public function hasPermission(string $permission): bool
{
    // super_admin and admin bypass all checks
    if (in_array($this->role, ['super_admin', 'admin'])) {
        return true;
    }

    // For dealer and sub_dealer: check JSONB permissions
    return ($this->permissions[$permission] ?? false) === true;
}

This method is called: - In controllers before performing actions - In policies (app/Policies/) - In React frontend via shared auth.permissions prop

Granting and Revoking Permissions

Permissions are managed via the User management UI (admin side). When an admin edits a dealer's permissions:

  1. The form sends the full permissions array
  2. UserController@update validates and updates users.permissions column
  3. The JSONB column is replaced atomically
  4. AuditLogger records the old and new permissions values
// Programmatic grant (in code):
$dealer->permissions = array_merge($dealer->permissions ?? [], [
    'billing.payments' => true,
]);
$dealer->save();

// Programmatic revoke:
$dealer->permissions = array_merge($dealer->permissions ?? [], [
    'customers.delete' => false,
]);
$dealer->save();

React Frontend Permission Checks

Permissions are shared to the React frontend via Inertia's shared props (set in HandleInertiaRequests middleware):

// In HandleInertiaRequests.php:
'auth' => [
    'user' => $request->user(),
    'permissions' => $request->user()?->permissions ?? [],
    'role' => $request->user()?->role,
    'isAdmin' => in_array($request->user()?->role, ['super_admin', 'admin']),
]

In React components:

import { usePage } from '@inertiajs/react';

export default function CreateCustomerButton() {
    const { auth } = usePage().props;

    // Check permission
    if (!auth.isAdmin && !auth.permissions?.['customers.create']) {
        return null; // Don't render the button
    }

    return <button>Add Customer</button>;
}

Important: Frontend permission checks are for UX only (hiding/showing buttons). They are never the sole security gate. All security enforcement happens server-side in controllers and policies.


Permission Keys Reference

Permission Key What It Controls Roles with Default Access
customers.view View full customer detail pages dealer (explicitly granted)
customers.create Create new customers dealer (explicitly granted)
customers.edit Edit customer info, change package/status dealer (explicitly granted)
customers.delete Soft-delete customers dealer (rarely granted)
customers.pppoe View/edit PPPoE credentials dealer (sometimes granted)
billing.invoices View customer invoices dealer (explicitly granted)
billing.payments Record payment transactions dealer (typically granted)
billing.recharge Renew customer subscription (extend expiry) dealer (explicitly granted)
billing.ledger View ledger entries dealer (explicitly granted)
billing.adjustments Make manual ledger adjustments admin only by default
reports.view Access reports and analytics module dealer (explicitly granted)
reports.export Export reports to Excel dealer (explicitly granted)
whatsapp.send Manually send WhatsApp messages dealer (sometimes granted)
whatsapp.templates Manage WhatsApp templates admin only by default
whatsapp.campaigns Create and run broadcast campaigns admin only by default
tickets.view View support tickets dealer (explicitly granted)
tickets.create Create new tickets dealer (explicitly granted)
tickets.manage Assign, close, resolve tickets dealer (sometimes granted)
network.nas View NAS devices admin only
network.olts View OLT devices admin only
network.signal View ONT signal data dealer (sometimes granted)
import.customers Bulk import customers via Excel admin only
audit.view View audit logs admin only
system.settings Edit system settings super_admin only
system.company Edit company profile super_admin only
system.crons Manage scheduled jobs super_admin / admin

This table represents the conceptual permission keys. Verify against the actual codebase for exact key strings used in controller and policy checks.


Customer Portal Details

Authentication Flow

Customer visits: https://your-server-ip/portal/login
→ Enters: username (pppoe_username or phone) + portal password
→ Portal guard authenticates against customers table (or portal_credentials table)
→ Session created under portal guard namespace
→ Redirect to /portal/dashboard

Portal Routes

All portal routes are prefixed with /portal and use the portal middleware group:

/portal/login          → portal login page
/portal/dashboard      → account overview
/portal/invoices       → invoice list
/portal/invoices/{id}  → invoice detail + PDF download
/portal/payments       → payment history
/portal/tickets        → support tickets
/portal/tickets/create → create new ticket
/portal/profile        → change portal password

Data Isolation

Portal users see strictly their own data. The portal guard's user object (the Customer model) is used to scope all queries:

// In portal controllers:
$customer = Auth::guard('portal')->user(); // The Customer model
$invoices = Invoice::where('customer_id', $customer->id)->get();
// No dealer scoping needed here — customer is the scope

Portal vs Admin Sessions

Portal sessions and admin sessions are completely separate: - Admin: SESSION_DRIVER=redis, key prefix admin_ - Portal: separate cookie name, separate session prefix - A user can be logged in as both an admin and a portal customer simultaneously in different browser tabs without conflict (though this would be unusual in practice)


For authorization code patterns and IDOR prevention, see 05_Authorization_Model.md. For system architecture context, see 01_System_Architecture.md.