Files
doormile_backend/routes/routes.go
Suriya 6738d37d7b feat: own the JetStream subject contract, and ingest jupiter rider telemetry
Half of this binary's js.Publish calls were bound to no stream at all.
The streams were declared only by an external Python script on another
machine (Birock/doormile-bookings/setup_jetstream.py) and had drifted
from the code: booking.cancelled, booking.outcome and
booking.assignment_failed had no stream, and CHAT declared the literal
"chat.room.closed" while chat.go publishes "chat.room.closed.<id>",
which it does not match. Every publish site is best-effort
(`if db.Js != nil` + warn-log), so those events were failing and being
dropped silently — every cancellation, delivery outcome and assignment
failure since the streams were created.

db.EnsureStreams now declares the streams at startup from a map that
sits next to the code that publishes, so the contract cannot drift
again. It only ever adds: existing streams keep their storage type,
retention, limits and every subject they already have. Nothing is
deleted. Losing the create race against a sibling replica is expected
and reconciles rather than erroring.

Alongside that, /internal/miler/* ingests rider telemetry still arriving
over the jupiter NATS chain. The forwarding worker holds no rider JWT —
the rider app is still jupiter-shaped — so LegacyMilerIdentity resolves
an identity from a header into c.Locals("userid") behind the existing
X-Internal-Key guard. That lets the routes reuse the miler handlers
unchanged instead of growing a parallel set that would drift.

Identity comes from a header, never the body: the telemetry handlers
overwrite a body-supplied userid precisely so one rider cannot write
another's GPS trail, and reading it from the body here would reopen that
from behind the internal key. MilerProfile.Legacyuserid (nullable,
indexed) maps a jupiter userid to a Doormile one.

Only fire-and-forget telemetry is exposed. Transactional actions stay
synchronous — a rider needs a real answer from pickup-complete, which a
queue in front of it cannot give.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 12:56:30 +05:30

449 lines
20 KiB
Go

package routes
import (
"context"
"time"
"doormile/config"
"doormile/controllers"
"doormile/db"
"doormile/internal/ws"
"doormile/middlewares"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/websocket/v2"
)
// authLimiter throttles credential-checking endpoints per IP. Miler and
// customer PINs are 4 digits — only 10,000 combinations — so without this an
// attacker can walk the whole keyspace in seconds. 10/minute keeps a genuine
// user's retries and typos working while making enumeration impractical.
func authLimiter() fiber.Handler {
return limiter.New(limiter.Config{
Max: 10,
Expiration: 1 * time.Minute,
LimitReached: func(c *fiber.Ctx) error {
return c.Status(fiber.StatusTooManyRequests).
JSON(fiber.Map{"success": false, "message": "too many attempts, please try again in a minute"})
},
})
}
func RegisterRoutes(app *fiber.App, cfg *config.Config) {
api := app.Group("/api/v1")
// One shared instance, so the 10/minute budget is spent across all
// credential endpoints combined — an attacker can't reset it by rotating
// between /login, /verify-pin and /reset-pin.
authThrottle := authLimiter()
// --------------------
// HEALTH & READINESS PROBES
// --------------------
api.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "OK", "time": time.Now()})
})
api.Get("/ready", func(c *fiber.Ctx) error {
dbStatus := "connected"
sqlDB, err := db.DB.DB()
if err != nil || sqlDB.Ping() != nil {
dbStatus = "unavailable"
}
redisStatus := "connected"
if db.Rdb == nil {
redisStatus = "unavailable"
} else {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if _, pingErr := db.Rdb.Ping(ctx).Result(); pingErr != nil {
redisStatus = "unavailable"
}
}
status := fiber.StatusOK
if dbStatus == "unavailable" || redisStatus == "unavailable" {
status = fiber.StatusServiceUnavailable
}
return c.Status(status).JSON(fiber.Map{
"status": "ready",
"checks": fiber.Map{
"postgres": dbStatus,
"redis": redisStatus,
},
})
})
// --------------------
// CUSTOMER APIS
// --------------------
customer := api.Group("/customer")
customer.Post("/register", controllers.RegisterCustomer(cfg))
customer.Post("/login", authThrottle, controllers.LoginCustomer)
customer.Post("/verify-pin", authThrottle, controllers.VerifyCustomerPin(cfg))
customer.Post("/reset-pin", authThrottle, controllers.ResetCustomerPin)
customer.Post("/send-email-otp", authThrottle, controllers.SendCustomerEmailOtp(cfg))
customer.Post("/verify-email-otp", authThrottle, controllers.VerifyCustomerEmailOtp())
// Authenticated Customer App routes
customerAuth := customer.Use(middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(9))
customerAuth.Get("/profile", controllers.GetCustomerProfile)
customerAuth.Put("/profile", controllers.UpdateCustomerProfile)
customerAuth.Get("/locations", controllers.GetCustomerLocations)
customerAuth.Post("/locations", controllers.CreateCustomerLocation)
customerAuth.Put("/locations/:id", controllers.UpdateCustomerLocation)
customerAuth.Delete("/locations/:id", controllers.DeleteCustomerLocation)
customerAuth.Put("/device-token", controllers.SaveCustomerDeviceToken)
customerAuth.Post("/bookings", middlewares.CityGateMiddleware, controllers.CreateCustomerBooking)
customerAuth.Get("/bookings", controllers.GetCustomerBookings)
customerAuth.Get("/bookings/:bookingid", controllers.GetCustomerBookingDetails)
customerAuth.Post("/bookings/:bookingid/cancel", controllers.CancelCustomerBooking)
customerAuth.Get("/bookings/:bookingid/price", controllers.GetCustomerBookingQuote)
customerAuth.Get("/track/:trackingno", controllers.TrackConsignment)
// --------------------
// MILER APIS
// --------------------
miler := api.Group("/miler")
miler.Post("/login", authThrottle, controllers.LoginMiler(cfg))
miler.Post("/verify-pin", authThrottle, controllers.VerifyMilerPin(cfg))
// PIN reset is console-operated, NOT self-service: ResetMilerPin overwrites
// the PIN given only a phone number, and phone numbers are the miler login
// identifier rather than a secret. Left unauthenticated, two calls
// (reset-pin then verify-pin) take over any miler account. Ops resets a
// rider's PIN on request instead, so this carries admin/manager/executive
// auth even though it sits under the /miler prefix.
miler.Post("/reset-pin", authThrottle,
middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(1, 3, 4),
controllers.ResetMilerPin)
// Authenticated Miler App routes
milerAuth := miler.Use(middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(5))
milerAuth.Get("/profile", controllers.GetMilerProfile)
milerAuth.Put("/profile", controllers.UpdateMilerProfile)
milerAuth.Put("/device-token", controllers.SaveMilerDeviceToken)
milerAuth.Put("/location", controllers.UpdateMilerLocation)
milerAuth.Put("/availability", controllers.UpdateMilerAvailability)
milerAuth.Get("/assignments", controllers.GetMilerAssignments)
milerAuth.Get("/assignments/:id", controllers.GetMilerAssignmentDetails)
milerAuth.Post("/assignments/:id/accept", controllers.AcceptMilerAssignment)
milerAuth.Post("/assignments/:id/reject", controllers.RejectMilerAssignment)
milerAuth.Post("/bookings/:bookingid/reached", controllers.BookingReachedCustomer)
milerAuth.Post("/bookings/:bookingid/parcel", controllers.BookingParcelConfirm)
milerAuth.Post("/bookings/:bookingid/payment", controllers.BookingPaymentCollect)
milerAuth.Post("/bookings/:bookingid/pickup-complete", controllers.BookingPickupComplete)
milerAuth.Post("/bookings/:bookingid/vehicle-required", controllers.BookingVehicleRequiredEscalate)
milerAuth.Post("/bookings/:bookingid/cancel", controllers.MilerCancelAssignment)
// Redis periodic telemetry and status logs
milerAuth.Post("/logs", controllers.CreateMilerPeriodicLog)
milerAuth.Get("/logs", controllers.GetMilerPeriodicLogs)
milerAuth.Post("/status", controllers.CreateMilerStatus)
milerAuth.Get("/status", controllers.GetMilerStatus)
// Redis consignment tracking and telemetry logs
milerAuth.Post("/consignments/logs", controllers.PublishConsignmentLogs)
milerAuth.Get("/consignments/logs/:consignmentid", controllers.GetConsignmentLogs)
milerAuth.Get("/consignments/userlogs/:userid", controllers.GetUserConsignmentLogs)
// Duty management
milerAuth.Post("/duty/start", controllers.MilerStartDuty)
milerAuth.Put("/duty/end", controllers.MilerEndDuty)
milerAuth.Get("/duty/current", controllers.MilerGetDutyStatus)
// Break management
milerAuth.Post("/breaks/start", controllers.MilerStartBreak)
milerAuth.Put("/breaks/end", controllers.MilerEndBreak)
// Miler's own bookings
milerAuth.Get("/bookings", controllers.MilerGetMyBookings)
// Delivery confirmation
milerAuth.Post("/consignments/:id/deliver", controllers.MilerDeliverConsignment)
milerAuth.Post("/consignments/:id/skip", controllers.MilerSkipDelivery)
// Earnings
milerAuth.Get("/earnings", controllers.MilerGetEarnings)
// Notifications
milerAuth.Get("/notifications", controllers.MilerGetNotifications)
milerAuth.Patch("/notifications/:id/read", controllers.MilerMarkNotificationRead)
// Support
milerAuth.Post("/support", controllers.MilerCreateTicket)
milerAuth.Get("/support", controllers.MilerGetTickets)
// --------------------
// ADMIN CONSOLE APIS — all open, no token required
// --------------------
admin := api.Group("/admin")
admin.Post("/login", authThrottle, controllers.LoginAdmin(cfg))
// Authenticated Admin Console routes
adminAuth := admin.Use(middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(1, 3, 4))
adminAuth.Get("/dashboard", controllers.GetAdminDashboard)
adminAuth.Get("/reports", controllers.GetAdminReports)
adminAuth.Get("/profile", controllers.GetAdminProfile)
adminAuth.Get("/me", controllers.GetAdminProfile)
adminAuth.Put("/profile/password", controllers.AdminChangePassword)
// App Users Management
adminAuth.Get("/users", controllers.GetAppUsers)
adminAuth.Post("/users", controllers.CreateAppUser)
adminAuth.Put("/users/:id", controllers.UpdateAppUser)
adminAuth.Delete("/users/:id", controllers.DeleteAppUser)
// Partner management (fleet/rider suppliers — distinct from tenants,
// which are the client companies Doormile delivers for)
adminAuth.Get("/partners", controllers.GetPartners)
adminAuth.Post("/partners", controllers.CreatePartner)
adminAuth.Get("/partners/:id", controllers.GetPartnerDetails)
adminAuth.Put("/partners/:id", controllers.UpdatePartner)
adminAuth.Delete("/partners/:id", controllers.DeletePartner)
// Tenant management
adminAuth.Get("/tenants", controllers.GetTenants)
adminAuth.Post("/tenants", controllers.CreateTenant)
adminAuth.Get("/tenants/:id", controllers.GetTenantDetails)
adminAuth.Put("/tenants/:id", controllers.UpdateTenant)
adminAuth.Delete("/tenants/:id", controllers.DeleteTenant)
adminAuth.Get("/tenants/:id/locations", controllers.GetTenantLocations)
adminAuth.Post("/tenants/:id/locations", controllers.CreateTenantLocation)
adminAuth.Put("/tenantlocations/:id", controllers.UpdateTenantLocation)
// Per-site performance — jupiter's getlocationsummary. Needs ?tenantid= for
// Doormile staff; a client login is already pinned to its own sites.
adminAuth.Get("/locations/summary", controllers.GetLocationSummary)
// Tenant customers
adminAuth.Get("/tenantcustomers", controllers.GetTenantCustomers)
adminAuth.Post("/tenantcustomers", controllers.CreateTenantCustomer)
adminAuth.Get("/tenantcustomers/:id", controllers.GetTenantCustomerDetails)
adminAuth.Put("/tenantcustomers/:id", controllers.UpdateTenantCustomer)
adminAuth.Delete("/tenantcustomers/:id", controllers.DeleteTenantCustomer)
// B2C App customers
adminAuth.Get("/customers", controllers.GetAdminCustomers)
adminAuth.Patch("/customers/:id", controllers.UpdateAdminCustomer)
// Hubs
adminAuth.Get("/hubs", controllers.GetHubs)
adminAuth.Post("/hubs", controllers.CreateHub)
adminAuth.Get("/hubs/:id", controllers.GetHubDetails)
adminAuth.Put("/hubs/:id", controllers.UpdateHub)
adminAuth.Delete("/hubs/:id", controllers.DeleteHub)
// Vehicles
adminAuth.Get("/vehicles", controllers.GetVehicles)
adminAuth.Post("/vehicles", controllers.CreateVehicle)
adminAuth.Get("/vehicles/:id", controllers.GetVehicleDetails)
adminAuth.Put("/vehicles/:id", controllers.UpdateVehicle)
adminAuth.Delete("/vehicles/:id", controllers.DeleteVehicle)
// Milers
adminAuth.Get("/milers", controllers.GetMilers)
// Registered before /milers/:id — Fiber matches in registration order, so
// the literal path has to come first or ":id" swallows "summary".
adminAuth.Get("/milers/summary", controllers.GetMilerSummary)
adminAuth.Post("/milers", controllers.CreateMiler)
adminAuth.Get("/milers/:id", controllers.GetMilerDetails)
adminAuth.Get("/milers/:id/logs", controllers.GetMilerLogs)
adminAuth.Get("/milers/:id/activity", controllers.GetMilerActivity)
adminAuth.Put("/milers/:id", controllers.UpdateMiler)
adminAuth.Put("/milers/:id/block", controllers.BlockMiler)
adminAuth.Put("/milers/:id/assign-vehicle", controllers.AssignMilerVehicle)
adminAuth.Post("/milers/:id/notify", controllers.AdminNotifyMiler)
// Bookings
adminAuth.Get("/bookings", controllers.GetAdminBookings)
adminAuth.Post("/expressbooking", middlewares.CityGateMiddleware, controllers.CreateExpressBooking)
adminAuth.Post("/expressbooking/bulk", middlewares.CityGateMiddleware, controllers.AdminBulkCreateBookings)
adminAuth.Get("/bookings/:id", controllers.GetAdminBookingDetails)
adminAuth.Get("/bookings/:id/track", controllers.GetAdminBookingTrack)
adminAuth.Post("/bookings/:id/assign-miler", controllers.AdminAssignMiler)
adminAuth.Post("/bookings/:id/assign-vehicle", controllers.AdminAssignVehicle)
adminAuth.Put("/bookings/:id/status", controllers.AdminUpdateBookingStatus)
adminAuth.Post("/bookings/:id/cancel", controllers.AdminCancelBooking)
adminAuth.Post("/bookings/bulk-cancel", controllers.AdminBulkCancelBookings)
// Consignments
adminAuth.Get("/consignments", controllers.GetAdminConsignments)
adminAuth.Get("/consignments/:id", controllers.GetAdminConsignmentDetails)
adminAuth.Get("/consignments/:id/logs", controllers.GetAdminConsignmentLogs)
adminAuth.Get("/consignments/track/:trackingno", controllers.GetAdminConsignmentTracking)
adminAuth.Put("/consignments/:id/status", controllers.AdminUpdateConsignmentStatus)
// Tripsheets
adminAuth.Get("/tripsheets", controllers.GetTripsheets)
adminAuth.Post("/tripsheets", controllers.CreateTripsheet)
adminAuth.Get("/tripsheets/:id", controllers.GetTripsheetDetails)
adminAuth.Post("/tripsheets/:id/items", controllers.AddTripsheetItem)
adminAuth.Delete("/tripsheets/:id/items/:itemid", controllers.DeleteTripsheetItem)
adminAuth.Put("/tripsheets/:id/dispatch", controllers.DispatchTripsheet)
adminAuth.Put("/tripsheets/:id/arrive", controllers.ArriveTripsheet)
// Competitor branch survey data (from Enquiry.xlsx Sheet 1)
adminAuth.Get("/competitor-branches", controllers.GetCompetitorBranches)
adminAuth.Post("/competitor-branches", controllers.CreateCompetitorBranch)
adminAuth.Put("/competitor-branches/:id", controllers.UpdateCompetitorBranch)
adminAuth.Delete("/competitor-branches/:id", controllers.DeleteCompetitorBranch)
// Carrier pricing benchmarks (from Enquiry.xlsx Sheet 2)
adminAuth.Get("/carrier-pricing", controllers.GetCarrierPricing)
adminAuth.Post("/carrier-pricing", controllers.CreateCarrierPricing)
adminAuth.Put("/carrier-pricing/:id", controllers.UpdateCarrierPricing)
adminAuth.Delete("/carrier-pricing/:id", controllers.DeleteCarrierPricing)
// Pricing
adminAuth.Get("/pricing", controllers.GetPricing)
adminAuth.Post("/pricing", controllers.CreatePricing)
adminAuth.Put("/pricing/:id", controllers.UpdatePricing)
adminAuth.Delete("/pricing/:id", controllers.DeletePricing)
adminAuth.Post("/pricing/simulate", controllers.GetPricingQuoteSimulate)
adminAuth.Post("/pricing/quote", controllers.GetPricingQuoteSimulate)
// Doormile pricing bands
adminAuth.Get("/doormile-pricing", controllers.GetDoormilePricing)
adminAuth.Post("/doormile-pricing", controllers.CreateDoormilePricing)
adminAuth.Put("/doormile-pricing/:id", controllers.UpdateDoormilePricing)
adminAuth.Delete("/doormile-pricing/:id", controllers.DeleteDoormilePricing)
// Exceptions
adminAuth.Get("/exceptions", controllers.GetExceptions)
adminAuth.Post("/exceptions", controllers.CreateException)
adminAuth.Get("/exceptions/:id", controllers.GetExceptionDetails)
adminAuth.Put("/exceptions/:id/status", controllers.ResolveException)
// --------------------
// HUB CONSOLE APIS
// --------------------
hub := api.Group("/hub")
hub.Post("/login", authThrottle, controllers.HubStaffLogin(cfg))
// Authenticated Hub Console routes (role 6)
hubAuth := hub.Use(middlewares.HubStaffAuth(cfg))
hubAuth.Get("/dashboard", controllers.GetHubDashboardStats)
hubAuth.Get("/bookings/unassigned", controllers.GetHubUnassignedBookings)
hubAuth.Get("/bookings", controllers.GetHubBookingsRange)
hubAuth.Get("/inbound/today", controllers.GetHubInboundToday)
hubAuth.Get("/inbound", controllers.GetHubInboundRange)
hubAuth.Post("/bookings/:id/inbound", controllers.CreateInboundScan)
hubAuth.Post("/bookings/:id/assign-miler", controllers.HubAssignMiler)
hubAuth.Post("/bookings/:id/auto-assign", controllers.HubAutoAssign)
hubAuth.Post("/bookings/batch-assign", controllers.HubBatchAssign)
hubAuth.Get("/batches", controllers.GetHubBatches)
hubAuth.Post("/batches", controllers.CreateHubBatch)
hubAuth.Patch("/batches/:id/status", controllers.UpdateBatchStatus)
hubAuth.Get("/milers", controllers.GetHubMilers)
// Hub management — enforced Doormile-staff-only inside each handler
hubAuth.Post("/staff", controllers.CreateHubStaffAccount)
hubAuth.Get("/hubs", controllers.GetHubsInCity)
hubAuth.Post("/hubs", controllers.CreateCityHub)
hubAuth.Get("/tripsheets/in-transit", controllers.GetInTransitTripsheets)
hubAuth.Get("/inbound/vehicles", controllers.GetInboundVehicles)
hubAuth.Get("/activity", controllers.GetHubActivity)
hubAuth.Get("/zones", controllers.GetHubZones)
hubAuth.Get("/notifications", controllers.GetHubNotifications)
hubAuth.Patch("/notifications/:id/read", controllers.MarkNotificationRead)
hubAuth.Get("/routing/:trackingno", controllers.GetRoutingInfo)
hubAuth.Get("/rider-routes", controllers.GetAllRiderRoutes)
hubAuth.Get("/milers/:id/route", controllers.GetMilerRoute)
hubAuth.Get("/messages", controllers.GetHubMessages)
hubAuth.Get("/messages/:id", controllers.GetHubMessageThread)
hubAuth.Post("/messages/:id", controllers.SendHubMessage)
hubAuth.Patch("/messages/:id/read", controllers.MarkHubMessagesRead)
hubAuth.Get("/report", controllers.GetHubReport)
// Redis active user caching utilities — console/ops only. Open CRUD on a
// user cache with no authentication has no legitimate anonymous caller;
// the store is currently empty, so closing it breaks nothing.
redisUsers := api.Group("/utils/users/redis",
middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(1, 3, 4))
redisUsers.Post("/", controllers.CreateUserRedis)
redisUsers.Get("/", controllers.GetUserRedis)
redisUsers.Put("/:userid", controllers.UpdateUserRedis)
redisUsers.Delete("/:userid", controllers.DeleteUserRedis)
// --------------------
// DOORMILE PRICING — public check & meta (no auth)
// --------------------
api.Get("/pricing/meta", controllers.GetPricingMeta)
api.Post("/pricing/check", controllers.CheckPrice)
// --------------------
// BOOKING CACHE APIS — console/ops only
// --------------------
// Previously open "for testing", but live: listing the cache returns real
// bookings including customer delivery addresses, and the per-customer route
// takes a customer id straight from the URL. Behind console auth now.
bookingCache := api.Group("/bookings/cache",
middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(1, 3, 4))
bookingCache.Get("/", controllers.ListAllBookingsFromCache)
bookingCache.Get("/customer/:customer_id", controllers.GetCustomerBookingsFromCache)
bookingCache.Get("/:booking_id", controllers.GetBookingFromCache)
// --------------------
// CRM APIS — all open (field reps register from Flutter; web console reads without a separate CRM login)
// --------------------
crm := api.Group("/crm")
crm.Post("/clients", controllers.RegisterClient)
crm.Get("/clients", controllers.GetClients)
crm.Get("/clients/:id", controllers.GetClientDetails)
crm.Put("/clients/:id", controllers.UpdateClient)
crm.Delete("/clients/:id", controllers.DeleteClient)
// --------------------
// INTERNAL — machine-to-machine endpoints (API key auth, no JWT)
// --------------------
internal := api.Group("/internal", middlewares.InternalKeyAuth)
internal.Post("/notify", controllers.InternalNotify)
internal.Post("/bookings/:id/reassign", controllers.InternalReassign)
internal.Post("/agent-decisions", controllers.CreateAgentDecision)
internal.Get("/agent-decisions/similar", controllers.FindSimilarDecisions)
internal.Patch("/agent-decisions/:id/outcome", controllers.UpdateDecisionOutcome)
// Rider telemetry ingest for the jupiter NATS chain. The forwarding worker
// holds no rider JWT — the rider app is still jupiter-shaped and its token is
// jupiter's — so identity arrives as a header and LegacyMilerIdentity turns
// it into c.Locals("userid"). These are the *same handlers* the miler app
// hits under /miler; only the way identity is established differs, so
// validation and storage cannot drift between the two paths.
//
// Only fire-and-forget telemetry is exposed this way. Transactional actions
// (pickup-complete, deliver, payment) are deliberately absent: the rider
// needs a real answer from those, which a queue in front of them cannot give.
ingest := internal.Group("/miler", middlewares.LegacyMilerIdentity)
ingest.Post("/logs", controllers.CreateMilerPeriodicLog)
ingest.Post("/status", controllers.CreateMilerStatus)
ingest.Post("/consignments/logs", controllers.PublishConsignmentLogs)
ingest.Post("/breaks/start", controllers.MilerStartBreak)
ingest.Put("/breaks/end", controllers.MilerEndBreak)
// --------------------
// WEBSOCKET — live miler tracking (no auth, public tracking link)
// --------------------
app.Use("/ws", func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
return c.Next()
}
return fiber.ErrUpgradeRequired
})
app.Get("/ws/bookings/:bookingid/track", websocket.New(ws.TrackingHandler))
app.Get("/ws/bookings/:bookingid/chat", middlewares.WsChatAuth(cfg), websocket.New(ws.ChatHandler))
}