feat: rider visibility and tracking for the express console
A client login could list its own bookings but had no way to see what its riders were actually doing. jupiter's console gave them getridersummary and the rider/delivery logs; Doormile records all of it and exposed none of it. New console endpoints, all tenant-scoped: GET /admin/milers/summary roster with live state + range totals GET /admin/milers/:id/logs GPS trail from the Redis telemetry index GET /admin/milers/:id/activity one rider's assignments, duty and breaks GET /admin/consignments/:id/logs event history + telemetry + proof GET /admin/bookings/:id/track booking -> assignments -> parcel -> proof Also closes a rider IDOR: GetMilers scoped the roster to the caller's own fleet, but reading, editing, blocking, notifying or assigning a vehicle to a single rider by id did not, so a client login could walk the whole network's riders by incrementing the id. All five now go through assertMilerAccess. And the client dashboard no longer reports milers/customers/exceptions as zero — those have no tenant column, so they are counted through appusers, bookings and consignments respectively. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
292
docs/express-console-api.md
Normal file
292
docs/express-console-api.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Doormile Express Console — API reference
|
||||
|
||||
The console surface only (`/admin/*`). 89 routes: 1 login + 88 authenticated.
|
||||
Everything is under `https://api.doormile.com/api/v1`.
|
||||
|
||||
Miler-app, hub-console, customer-app and CRM routes are not in this document.
|
||||
|
||||
---
|
||||
|
||||
## Auth
|
||||
|
||||
```
|
||||
POST /api/v1/admin/login
|
||||
{ "email": "developer@doormile.com", "password": "admin@123" }
|
||||
```
|
||||
|
||||
Returns `{ success, token, user: { id, name, email, role, tenantid } }`.
|
||||
Send it on every other call as `Authorization: Bearer <token>`.
|
||||
|
||||
The token is a JWT carrying `userid`, `email`, `roleid`, `tenantid`, `configid`.
|
||||
Roles allowed on this group: **1 admin, 3 manager, 4 executive**. Anything else
|
||||
gets 401.
|
||||
|
||||
### Tenant scoping — read this before wiring any list screen
|
||||
|
||||
`tenantid` in the token decides what the account can see:
|
||||
|
||||
| Token `tenantid` | Who | Sees |
|
||||
|---|---|---|
|
||||
| `0` / null | Doormile's own staff | everything, all tenants |
|
||||
| set (e.g. `13`) | a client's console login | only that tenant's rows |
|
||||
|
||||
Scoping is applied server-side. A client login **cannot** widen it by sending
|
||||
`?tenantid=` or a body `tenantid` — on writes the server overwrites the field
|
||||
with the token's tenant. Build the UI as if the API returns exactly what the
|
||||
account is allowed to see, because it does.
|
||||
|
||||
Note the group is registered under a stale `// all open, no token required`
|
||||
comment in `routes.go`; the comment is wrong, `AuthMiddleware` is applied.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard, profile, reports
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/admin/dashboard` | counts + today's numbers |
|
||||
| GET | `/admin/reports` | `?from=YYYY-MM-DD&to=YYYY-MM-DD`, defaults to today (IST) |
|
||||
| GET | `/admin/profile` | current account |
|
||||
| GET | `/admin/me` | alias of the above |
|
||||
| PUT | `/admin/profile/password` | `{ "current_password": "...", "new_password": "..." }` — snake_case |
|
||||
|
||||
## App users (staff logins)
|
||||
|
||||
| Method | Path |
|
||||
|---|---|
|
||||
| GET | `/admin/users` |
|
||||
| POST | `/admin/users` |
|
||||
| PUT | `/admin/users/:id` |
|
||||
| DELETE | `/admin/users/:id` |
|
||||
|
||||
## Partners (fleet / rider suppliers)
|
||||
|
||||
Not the same thing as a tenant. A partner supplies vehicles and riders; a tenant
|
||||
is a client Doormile delivers for.
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/partners` | |
|
||||
| POST | `/admin/partners` | `{ partnername, partnertypeid, contactno, status }` |
|
||||
| GET | `/admin/partners/:id` | |
|
||||
| PUT | `/admin/partners/:id` | |
|
||||
| DELETE | `/admin/partners/:id` | hard delete — no soft-delete column |
|
||||
|
||||
## Tenants (client companies)
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/tenants` | |
|
||||
| POST | `/admin/tenants` | `{ tenantname, primaryemail, primarycontact, status, requiredeliveryotp }` |
|
||||
| GET | `/admin/tenants/:id` | |
|
||||
| PUT | `/admin/tenants/:id` | `requiredeliveryotp` is a pointer — omit it to leave the setting alone |
|
||||
| DELETE | `/admin/tenants/:id` | hard delete |
|
||||
| GET | `/admin/tenants/:id/locations` | the client's sites (kitchens, branches, depots) |
|
||||
| POST | `/admin/tenants/:id/locations` | `{ locationname, address, city, state, pincode, latitude, longitude, isprimary, status }` |
|
||||
| PUT | `/admin/tenantlocations/:id` | note: **not** nested under the tenant |
|
||||
|
||||
`requiredeliveryotp` is opt-in per tenant and **off by default**. DailyGrubs runs
|
||||
without delivery OTP by decision.
|
||||
|
||||
## Tenant customers (a client's own end customers)
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/tenantcustomers` | |
|
||||
| POST | `/admin/tenantcustomers` | `{ firstname, lastname, phone, email }` |
|
||||
| GET | `/admin/tenantcustomers/:id` | |
|
||||
| PUT | `/admin/tenantcustomers/:id` | |
|
||||
| DELETE | `/admin/tenantcustomers/:id` | |
|
||||
|
||||
## B2C app customers
|
||||
|
||||
| Method | Path |
|
||||
|---|---|
|
||||
| GET | `/admin/customers` |
|
||||
| PATCH | `/admin/customers/:id` |
|
||||
|
||||
Tenant-scoped through their bookings — a client login sees only customers who
|
||||
have ordered through them.
|
||||
|
||||
## Hubs
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/hubs` | |
|
||||
| POST | `/admin/hubs` | `{ hubname, hubtype, applocationid, contactno, address, latitude, longitude, pincode, status }` |
|
||||
| GET | `/admin/hubs/:id` | |
|
||||
| PUT | `/admin/hubs/:id` | |
|
||||
| DELETE | `/admin/hubs/:id` | soft delete |
|
||||
|
||||
`hubtype`: `sorting_center` \| `delivery_hub`. `applocationid` is the city —
|
||||
Nagercoil is 5; read the rest from `GET /admin/hubs` rather than hardcoding.
|
||||
|
||||
## Vehicles
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/vehicles` | |
|
||||
| POST | `/admin/vehicles` | `{ vehicleno, vehicletype, maxweight, maxvolume, partnerid, batterypercentage, status }` |
|
||||
| GET | `/admin/vehicles/:id` | |
|
||||
| PUT | `/admin/vehicles/:id` | |
|
||||
| DELETE | `/admin/vehicles/:id` | soft delete |
|
||||
|
||||
## Milers (riders)
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/milers` | tenant-scoped |
|
||||
| POST | `/admin/milers` | see below |
|
||||
| GET | `/admin/milers/:id` | |
|
||||
| PUT | `/admin/milers/:id` | |
|
||||
| PUT | `/admin/milers/:id/block` | |
|
||||
| PUT | `/admin/milers/:id/assign-vehicle` | |
|
||||
| POST | `/admin/milers/:id/notify` | `{ title, message }` — `:id` is the **milerprofileid** |
|
||||
|
||||
```jsonc
|
||||
POST /admin/milers
|
||||
{
|
||||
"authname": "Murali",
|
||||
"email": "murali@dailygrubs.com",
|
||||
"contactno": "9876543210",
|
||||
"password": "1234",
|
||||
"displayname": "Murali S",
|
||||
"tenantid": 13,
|
||||
"defaultvehicletype": "Bike",
|
||||
"applocationid": 1,
|
||||
"hubid": 4 // optional; without it the rider is invisible to the hub console
|
||||
// configid defaults to 1001 — the partition the miler app logs in against.
|
||||
// Do not override it. Riders created without it could never log in.
|
||||
}
|
||||
```
|
||||
|
||||
## Bookings
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/admin/bookings` | tenant-scoped list |
|
||||
| POST | `/admin/expressbooking` | create one — passes CityGate |
|
||||
| POST | `/admin/expressbooking/bulk` | `{ "bookings": [ ... ] }`, max 200, per-row results |
|
||||
| GET | `/admin/bookings/:id` | 403 if outside your tenant |
|
||||
| POST | `/admin/bookings/:id/assign-miler` | |
|
||||
| POST | `/admin/bookings/:id/assign-vehicle` | |
|
||||
| PUT | `/admin/bookings/:id/status` | |
|
||||
| POST | `/admin/bookings/:id/cancel` | |
|
||||
| POST | `/admin/bookings/bulk-cancel` | |
|
||||
|
||||
```jsonc
|
||||
POST /admin/expressbooking
|
||||
{
|
||||
"tenantid": 13, // required; forced to your own tenant on client logins
|
||||
"pickuplocationid": 15, // a stored kitchen/branch — fills address, pincode and
|
||||
// coords for you, and is what makes per-site reporting work
|
||||
"customer_phone": "9876543210", // creates a Guest customer if unknown
|
||||
"customer_name": "Ramesh",
|
||||
"deliveryaddress": "12 Cross Cut Road, Gandhipuram",
|
||||
"deliverypincode": "641012",
|
||||
"deliverycity": "Coimbatore",
|
||||
"deliverylatitude": 11.0168,
|
||||
"deliverylongitude": 76.9558,
|
||||
"service_option": "Fast", // Normal | Fast | Superfast
|
||||
"finalprice": 120, // the order amount the tenant pays — passed through
|
||||
"notes": "Ring the bell",
|
||||
"parcels": [
|
||||
{ "itemcategory": "Food", "itemdescription": "2 meal boxes", "declaredvalue": 350 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules worth knowing:
|
||||
- `parcels` must be non-empty and `tenantid` must exist.
|
||||
- Pickup address + pincode are required **unless** `pickuplocationid` supplies them.
|
||||
- A `pickuplocationid` belonging to another tenant is rejected.
|
||||
- **CityGate**: the pickup pincode prefix must be an open city — `641`
|
||||
Coimbatore, `600` Chennai, `560` Bengaluru, `500` Hyderabad, `629` Nagercoil.
|
||||
Any other prefix is refused at the middleware, before the handler runs.
|
||||
- Matching 3-digit pickup and delivery prefixes = hyperlocal, and the parcel goes
|
||||
straight to `Out_for_Delivery` at pickup instead of routing via a hub.
|
||||
- Auto-assignment fires after commit as a background retry loop (5 attempts,
|
||||
2 min apart). The response returns before a rider is attached.
|
||||
|
||||
## Consignments
|
||||
|
||||
| Method | Path |
|
||||
|---|---|
|
||||
| GET | `/admin/consignments` |
|
||||
| GET | `/admin/consignments/:id` |
|
||||
| GET | `/admin/consignments/track/:trackingno` |
|
||||
| PUT | `/admin/consignments/:id/status` |
|
||||
|
||||
## Tripsheets (hub-to-hub transport)
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/tripsheets` | |
|
||||
| POST | `/admin/tripsheets` | `{ sourcehubid, destinationhubid, vehicleid, driveruserid }` |
|
||||
| GET | `/admin/tripsheets/:id` | |
|
||||
| POST | `/admin/tripsheets/:id/items` | `{ consignmentid }` |
|
||||
| DELETE | `/admin/tripsheets/:id/items/:itemid` | |
|
||||
| PUT | `/admin/tripsheets/:id/dispatch` | |
|
||||
| PUT | `/admin/tripsheets/:id/arrive` | |
|
||||
|
||||
## Pricing
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/admin/pricing` | tenant pricing rules |
|
||||
| POST | `/admin/pricing` | `{ tenantid, applocationid, vehicletype, baseprice, baseweight, priceperkg, basedistance, priceperkm, handlingcharges, effectivefrom, effectiveto, currency, priority, status }` |
|
||||
| PUT | `/admin/pricing/:id` | |
|
||||
| DELETE | `/admin/pricing/:id` | |
|
||||
| POST | `/admin/pricing/simulate` | quote without creating anything |
|
||||
| POST | `/admin/pricing/quote` | same handler as simulate |
|
||||
| GET | `/admin/doormile-pricing` | Doormile's own bands |
|
||||
| POST | `/admin/doormile-pricing` | |
|
||||
| PUT | `/admin/doormile-pricing/:id` | |
|
||||
| DELETE | `/admin/doormile-pricing/:id` | soft delete |
|
||||
|
||||
## Exceptions
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/exceptions` | |
|
||||
| POST | `/admin/exceptions` | `{ consignmentid, tripsheetid, hubid, exceptiontype, severity, description }` |
|
||||
| GET | `/admin/exceptions/:id` | |
|
||||
| PUT | `/admin/exceptions/:id/status` | `{ resolution, status }` — `Resolved` \| `Closed` |
|
||||
|
||||
`exceptiontype`: `Lost`, `Damaged`, `Misrouted`, `Receiver_Refused`,
|
||||
`Missing_Contents`, `Undeliverable`. `severity`: `Low`, `Medium`, `High`,
|
||||
`Critical`.
|
||||
|
||||
## Competitive intel
|
||||
|
||||
| Method | Path |
|
||||
|---|---|
|
||||
| GET/POST | `/admin/competitor-branches` |
|
||||
| PUT/DELETE | `/admin/competitor-branches/:id` |
|
||||
| GET/POST | `/admin/carrier-pricing` |
|
||||
| PUT/DELETE | `/admin/carrier-pricing/:id` |
|
||||
|
||||
---
|
||||
|
||||
## Conventions across every endpoint
|
||||
|
||||
- **Envelope**: `{ "success": true, "data": ... }` on success,
|
||||
`{ "success": false, "message": "..." }` on failure. Lists add `total`,
|
||||
paginated lists add `page`.
|
||||
- **Pagination**: `?pageno=1&pagesize=100`. Default 500, cap 1000.
|
||||
- **Rate limits**: 300/min per IP globally, 10/min shared across all credential
|
||||
endpoints. Behind the ingress this keys on the proxy IP unless
|
||||
`TRUSTED_PROXIES` is set.
|
||||
- **Timestamps** are IST (`Asia/Kolkata`) wall-clock in `timestamp without time
|
||||
zone` columns. Send dates as `YYYY-MM-DD`, not epochs.
|
||||
- **Soft delete** exists on Hub, Vehicle, Consignment, Tripsheet, TripsheetItem,
|
||||
ConsignmentException, DoormilePricing, CarrierPricing. Partner and Tenant are
|
||||
hard-deleted.
|
||||
|
||||
## Not exercised yet
|
||||
|
||||
Roughly half these routes have never had a real request against them. Exercised
|
||||
end-to-end so far: login, dashboard, reports, tenants + locations, milers
|
||||
(create/list/notify), expressbooking (single), bookings list/detail,
|
||||
assign-miler, consignments, profile password. Treat the rest as written but
|
||||
unproven.
|
||||
Reference in New Issue
Block a user