Skip to content

Agent Types & Permissions

Agent types are configurable role templates that determine what systems and permissions a given agent has access to. Rather than hardcoding every permission, admins can create named agent types (e.g., “Document Receiver”, “VFS Agent”, “Ticketing Agent”) and assign them to users.

agent_types
id INT
name TEXT (unique)
description TEXT
permissions TEXT ← JSON array of permission strings
systems TEXT ← JSON array: ["VFS", "TICKETING"]
category TEXT ← legacy field, maps to first system
isActive INT (1 = active)
{
"name": "VFS Agent",
"description": "Handles physical document collection and VFS submission",
"systems": ["VFS"],
"permissions": [
"DOCUMENT_RECEIVER",
"DOCUMENT_AT_SHANVI",
"VFS_RECEIVED",
"REJECT_TASK"
]
}
PermissionWhat it unlocks
MANAGE_TICKETSUpload and edit FD tickets
VIEW_ALL_TICKETSSee all agents’ tickets (not just own)
DOCUMENT_RECEIVERMove VFS tasks to DOCUMENT_RECEIVER / DISPATCHED_TO_SHANVI
DOCUMENT_AT_SHANVIMove VFS tasks to DOCUMENT_AT_SHANVI
VFS_RECEIVEDMove VFS tasks to VFS_RECEIVED
VFS_AFTER_SHANVIMove VFS tasks to VFS_COLLECTED / VFS_AFTER_SHANVI
CONSULTANCY_RECEIVEDMove VFS tasks to CONSULTANCY_RECEIVED
TASK_CLOSEClose VFS tasks
REJECT_TASKReject VFS tasks
CREATE_TASKCreate new VFS tasks
VIEW_ALL_DOCUMENTSSee all VFS tasks (not just own)
SystemWhat it grants access to
VFSVFS tracking pipeline
TICKETINGFD ticket upload and management

HEAD_OFFICE is a hardcoded agent type that has all permissions and sees all data. It is not stored in agent_types — it is recognized by name at runtime in multiple places:

// From routes/auth.js enrichUserData()
const nameUpper = userData.agentType.toUpperCase().trim().replace(/_/g, ' ');
if (nameUpper === 'HEAD OFFICE' || nameUpper === 'HEAD_OFFICE') {
resolve({
...userData,
permissions: ['MANAGE_TICKETS', 'VIEW_ALL_TICKETS', 'DOCUMENT_RECEIVER', ...all permissions],
systems: ['VFS', 'TICKETING']
});
}

Design smell: Multiple places in the codebase check agentType.toUpperCase().trim().replace(/_/g, ' ') === 'HEAD OFFICE' individually — this is fragile and prone to drift if the name changes. A better approach is to store HEAD_OFFICE as a seeded row in agent_types with all permissions, and rely on the permissions lookup everywhere.

How permissions are checked in the backend

Section titled “How permissions are checked in the backend”

When an agent makes a request, the backend fetches their agentType name, looks up the agent_types table, and checks whether the required permission is in the JSON array:

// Simplified from routes/tickets.js checkCanManageTickets()
const agentTypeRecord = await get('SELECT * FROM agent_types WHERE name = ?', [agentTypeName]);
const systems = JSON.parse(agentTypeRecord.systems || '[]');
const permissions = JSON.parse(agentTypeRecord.permissions || '[]');
if (systems.includes('TICKETING') || permissions.includes('MANAGE_TICKETS')) {
return true; // can manage tickets
}

Note that this lookup is only done for the TICKETING system currently. VFS permission checks appear to happen primarily at the frontend level (checking userPermissions from the auth context), which means the backend does not fully enforce VFS permissions. This is a security gap — backend enforcement should be added for VFS status transitions.

Agent types are managed by ADMIN users in the dashboard Settings tab.

API endpoints (all ADMIN only):

MethodPathDescription
GET/api/admin/agent-typesList all agent types
POST/api/admin/agent-typesCreate agent type
PUT/api/admin/agent-types/:idUpdate agent type
DELETE/api/admin/agent-types/:idDelete agent type

Done via the role update endpoint:

PUT /api/admin/users/:id/role
{ "role": "AGENT", "agentType": "VFS Agent", "kyc_status": "APPROVED" }

The agentType field stores the name string of the agent type, not its ID. This means renaming an agent type in agent_types will silently break all users assigned to it — their agentType field will no longer match any row.

Recommendation: Store agentTypeId (foreign key) instead of the name string. This makes renames safe and queries simpler.