Admin, Approvals & System API
Staff users, roles & permissions, audit, communications, internal settings, finance config, storage upload, and approvals routes.
Users (staff)
Handlers: handleUsers at src/lib/api.ts:12968,
handleUsersById at src/lib/api.ts:13056,
handleUserPassword at src/lib/api.ts:13075,
handleUserRoles at src/lib/api.ts:13103.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/users |
GET | (read-only) | List staff users + their roles |
/users |
POST | admin.users.create |
Provision staff user (email must end in @alhuda.co.in or @alhudatravels.in) |
/users/:id |
DELETE | admin.users.delete |
Soft-delete user |
/users/:id/password |
POST | Admin resets user password via Supabase admin API | |
/users/:id/roles |
POST | Replace user's role assignments |
POST /users
Cite: src/lib/api.ts:13021.
Calls the admin-users Supabase edge function (requires the service role key server-side).
Enforces domain restriction on staff email: only @alhuda.co.in and @alhudatravels.in
are accepted.
Returns — { id, tempPassword }.
POST /users/:id/roles
Cite: src/lib/api.ts:13103. Accepts body shapes:
{ roleId }— single UUID{ role }— single name or UUID{ roles: [...] }— array of names or UUIDs
Resolves names to IDs against the Role table. Deletes existing UserRole rows and
inserts the new ones in one transaction.
Permissions admin
Handler: handlePermissions at src/lib/api.ts:12519. The first
line is await requireAdminUser() — every sub-route is additionally behind admin gating.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/permissions |
GET | requireAdminUser |
List the permission catalog |
/permissions/roles |
GET | requireAdminUser |
Roles with their permission grants |
/permissions/roles/:roleId |
PUT | admin.permissions.edit |
Replace a role's RolePermission rows |
/permissions/users |
GET | requireAdminUser |
Users with roles + overrides |
/permissions/users/:userId |
PUT | admin.permissions.edit |
Replace a user's role assignments and UserPermission overrides |
Agents (partner admin)
See Customers — GET/POST/PATCH/DELETE /agents are documented
there. Admin-only uses of /agents (bulk status changes, partner approval on pending
signups) flow through PATCH /agents/:id with status='active'|'inactive'|'suspended'.
Audit log
Handler: handleAudit at src/lib/api.ts:12457.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/audit |
GET | (read-only) | Audit log with optional ?entityType=&entityId=; resolves user names and booking numbers |
/audit |
POST | finance.create |
Append an external audit entry |
/admin/audit-log |
GET, POST | same as /audit |
Alias |
Permission is finance.create — historical
Writing to the audit log requires finance.create by convention; a future refactor may
introduce a dedicated audit.write permission.
Communications
Handlers: handleOperationsCommunications at src/lib/api.ts:13180,
handleCommunicationQueue at src/lib/api.ts:3181.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/operations/communications |
GET | (read-only) | Filter by ?entityType=&entityId=&limit= |
/operations/communications |
POST | admin.users.edit |
Log a communication; optionally dispatch via WhatsApp/email now or schedule for later |
/communications/queue |
GET | (read-only) | Inspect the scheduled communications queue |
Dispatch semantics
Cite: src/lib/api.ts:13200-13360. When channel is whatsapp or
email:
- If
sendNow === false, requires a futurescheduledFordatetime. Inserts intoCommunicationQueuewithstatus='pending'. - If
sendNow !== false, dispatches viasendMailer({ type, channel, phone|to, templateData })and logs intoCommunicationsLogwith the provider name tagged in notes. - Phone / email are resolved via
resolveCommunicationTarget(entityType, entityId)from the related booking / agent / customer, or taken from the body directly.
Daily stats
Inline handler at src/lib/api.ts:28065. GET /admin/daily-stats
with optional ?date=YYYY-MM-DD (defaults to today).
Returns — bookings created/approved/rejected/totalAmount, payments count/totalReceived/ verified/pending, customers created/total, groups active/departing7d, finance journal count + totals (debit/credit), visa applied/issued/pending. Read-only; no permission check ().
Currencies
Inline handler at src/lib/api.ts:28124. GET /admin/currencies
returns active rows from the currencies table (code, name, symbol).
Finance config
Inline handler at src/lib/api.ts:28166.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/admin/finance-config |
GET | (read-only) | Read FinanceConfig singleton (sanitized for client) |
/admin/finance-config |
PUT | finance.edit |
Update allowed fields (prefixes, default currency, GST rate, GL account IDs); invalidates cache |
The allowlist of mutable fields is fixed (see src/lib/api.ts:28173-28190). Fields not in
the list are ignored. After update, assertFinanceConfigAccountTypes validates that every
GL account ID maps to the right account type (ASSET/LIABILITY/etc).
Communication settings
Inline handler at src/lib/api.ts:28205.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/admin/communication-settings |
GET | (read-only) | List all settings keys |
/admin/communication-settings |
PUT | admin.edit |
Upsert { settings: [{key, value}] } |
Stores sender-side credentials and template defaults (SMTP, WhatsApp API keys, etc) in the
CommunicationSetting table keyed by key.
Storage upload
Inline handler handleStorageUpload at src/lib/api.ts:27724.
| Route | Method | Permission | Purpose |
|---|---|---|---|
/storage/upload |
POST | authenticated (no named permission) | Upload a file; returns { storageKey, provider, fileName } |
Returns — reference tokens used by ticket/visa/customer document handlers. The file is stored in the configured provider (Supabase Storage or Google Drive) and ownership is enforced by the downstream handler that associates the upload with an entity.
Requests (service requests)
See Bookings — /requests, /requests/:id,
/requests/:id/notes are documented there.
Portals
Partner (/portals/agent/*) and customer (/portals/customer/*) portal routes are
documented in Bookings. They are JWT-authenticated
and row-scoped by resolving the caller's Agent or Customer row — no permission checks.
Internal / maintenance endpoints
| Route | Method | Permission | Purpose |
|---|---|---|---|
/finance/ledger |
POST | admin.edit |
Rebuild derived ledger rows (mode: 'rebuild_quota_blocks') |
/finance/stock-adjustment |
POST | finance.create |
Manual inventory write-up / write-down |
/finance/supplier-transactions |
GET | finance.view |
Dump of all SupplierTransaction rows |
Deprecated endpoints
These routes throw immediately — do not use.
| Route | Method | Error |
|---|---|---|
/accounts |
POST | 400 — "Create GL accounts from Finance → Accounts..." |
/accounts/:id |
PATCH | 400 — same |
Use /finance/accounts instead (see Finance).
Unmigrated endpoints
When the dispatch cascade falls through without a match, the router throws
ApiError(501, { message: 'Unmigrated endpoint: <METHOD> <path>. This UI route still
expects the old /api backend. Migrate it to supabase.from(...).' }). Cite:
src/lib/api.ts:28235.
Typical cause — a frontend service calling an old REST path before someone adds the
dispatch entry. The fix is to add the handler in _apiFetchInternal per the checklist in
the Overview.