589 lines
29 KiB
Go
589 lines
29 KiB
Go
package routes
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"doormile/config"
|
|
"doormile/controllers"
|
|
"doormile/db"
|
|
"doormile/internal/sms"
|
|
"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,
|
|
// Reported, but deliberately NOT gating readiness: the miler and
|
|
// console surfaces work perfectly without SMS. It is here because
|
|
// 'the OTP never arrived' was answerable only by reading code, and
|
|
// for this service's whole life the answer has been that no gateway
|
|
// was ever registered.
|
|
"sms": fiber.Map{
|
|
"transport": sms.Transport(),
|
|
"configured": sms.Configured(),
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
// --------------------
|
|
// CUSTOMER APIS — doormile_cx
|
|
// --------------------
|
|
// The customer books a PICKUP, not a shipment: one visit, 1..N
|
|
// destinations, and a tracking number per destination minted only when the
|
|
// miler completes the pickup. Everything under /customer/* answers in the
|
|
// customer envelope ({success, data, message}; errors carry error.code) —
|
|
// auth included, deliberately unlike /miler/verify-pin, whose payload sits
|
|
// outside `data` and cost the miler client a release to discover.
|
|
//
|
|
// This replaces the PIN-based customer surface (register / login /
|
|
// verify-pin / reset-pin, and the single-destination booking create, list,
|
|
// detail and cancel). Those were retired rather than moved: a booking with
|
|
// one delivery address in its own columns is not a shape the product has
|
|
// any more.
|
|
customer := api.Group("/customer")
|
|
|
|
// Auth — a 4-digit code to a phone or an email address, no password
|
|
// anywhere. Throttled on the shared budget with every other credential
|
|
// endpoint so an attacker cannot reset it by rotating between them.
|
|
customer.Post("/auth/otp/request", authThrottle, controllers.CxRequestOtp(cfg))
|
|
customer.Post("/auth/signup", authThrottle, controllers.CxSignup(cfg))
|
|
// Idempotent: the client retries on flaky networks, and a replayed verify
|
|
// must return the original session rather than mint a second one.
|
|
customer.Post("/auth/otp/verify", authThrottle, middlewares.Idempotency(), controllers.CxVerifyOtp(cfg))
|
|
customer.Post("/auth/refresh", authThrottle, controllers.CxRefresh(cfg))
|
|
|
|
// Interim PIN auth, until the SMS/OTP gateway is live (see internal/sms).
|
|
// Same shape as the miler flow: /login reports whether a PIN is set, then the
|
|
// app routes to /set-pin (first time / new account) or /verify-pin (returning).
|
|
// set-pin can't overwrite an existing PIN, so it's safe unauthenticated here.
|
|
customer.Post("/auth/login", authThrottle, controllers.CxPinLogin)
|
|
customer.Post("/auth/set-pin", authThrottle, controllers.CxSetPin(cfg))
|
|
customer.Post("/auth/verify-pin", authThrottle, controllers.CxVerifyPin(cfg))
|
|
|
|
// Serviceability and configuration are read before sign-in: the booking
|
|
// form is explorable without an account, and gating the state picker behind
|
|
// auth would make the app's first screen a login wall.
|
|
customer.Get("/serviceability/states", controllers.GetCxStates)
|
|
customer.Get("/serviceability/states/:stateCode/districts", controllers.GetCxDistricts)
|
|
customer.Get("/pickup-slots", controllers.GetCxPickupSlots)
|
|
customer.Get("/config/booking-limits", controllers.GetCxBookingLimits)
|
|
|
|
// Authenticated Customer App routes
|
|
customerAuth := customer.Use(middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(9))
|
|
|
|
customerAuth.Get("/auth/me", controllers.CxMe)
|
|
customerAuth.Post("/auth/logout", controllers.CxLogout)
|
|
|
|
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)
|
|
|
|
// Push registration. One row per device token, not one column per customer:
|
|
// a phone and a tablet both have to receive the delivery notification.
|
|
customerAuth.Post("/devices", controllers.RegisterCxDevice)
|
|
customerAuth.Delete("/devices/:token", controllers.UnregisterCxDevice)
|
|
|
|
// Places are proxied, never keyed: the legacy rider app shipped a Maps key
|
|
// in the binary and it had to be revoked. The customer app is handed
|
|
// results, not credentials.
|
|
customerAuth.Get("/places/reverse-geocode", controllers.ReverseGeocodeCx(cfg))
|
|
customerAuth.Get("/places/search", controllers.SearchCxPlaces(cfg))
|
|
|
|
// Called on every route and package-count change, so it is cheap and
|
|
// cacheable — and a failed estimate never blocks a booking.
|
|
customerAuth.Post("/fare/estimate", controllers.EstimateCxFare)
|
|
|
|
// Idempotency-Key on create: the client retries over bad networks and a
|
|
// duplicate pickup is unacceptable.
|
|
customerAuth.Post("/bookings", middlewares.CityGateMiddleware, middlewares.Idempotency(), controllers.CreateCxBooking)
|
|
customerAuth.Get("/bookings", controllers.GetCxBookings)
|
|
customerAuth.Get("/bookings/:reference", controllers.GetCxBookingDetail)
|
|
customerAuth.Post("/bookings/:reference/cancel", controllers.CancelCxBooking)
|
|
customerAuth.Patch("/bookings/:reference/destinations/:index", controllers.PatchCxDestination)
|
|
|
|
// One order by tracking number, for push deep links (doormile://track/…).
|
|
customerAuth.Get("/orders/:trackingId", controllers.GetCxOrder)
|
|
|
|
// QA only. Refused outright unless ENV is non-production AND
|
|
// CX_ALLOW_STAGE_OVERRIDE=true — two independent switches, because either
|
|
// one being wrong in production would let any customer mark their own
|
|
// parcel delivered. It exists so every tracking state is reachable for
|
|
// design QA and the app's debug stepper can be deleted.
|
|
customerAuth.Post("/ops/bookings/:reference/stage", controllers.ForceCxStage)
|
|
|
|
// --------------------
|
|
// MILER APIS
|
|
// --------------------
|
|
miler := api.Group("/miler")
|
|
miler.Post("/login", authThrottle, controllers.LoginMiler(cfg))
|
|
miler.Post("/verify-pin", authThrottle, controllers.VerifyMilerPin(cfg))
|
|
// First-login PIN creation is self-service (unlike reset-pin below), and safe
|
|
// to leave unauthenticated because SetMilerPin refuses to overwrite an
|
|
// existing PIN — it only works on an account that has none yet. authThrottle
|
|
// still caps abuse of the phone-number probe.
|
|
miler.Post("/set-pin", authThrottle, controllers.SetMilerPin(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)
|
|
// Partial correction of pickup/delivery address, pincode, coords, city at the
|
|
// door. Only non-empty fields are written; rejected once picked up.
|
|
milerAuth.Patch("/bookings/:bookingid/addresses", controllers.BookingUpdateAddresses)
|
|
milerAuth.Post("/bookings/:bookingid/parcel", controllers.BookingParcelConfirm)
|
|
// Money and state-conversion mutations are idempotent: a rider retry over a
|
|
// bad connection with the same Idempotency-Key replays the first response
|
|
// instead of collecting COD twice or minting a second consignment.
|
|
milerAuth.Post("/bookings/:bookingid/payment", middlewares.Idempotency(), controllers.BookingPaymentCollect)
|
|
milerAuth.Post("/bookings/:bookingid/pickup-complete", middlewares.Idempotency(), controllers.BookingPickupComplete)
|
|
milerAuth.Post("/bookings/:bookingid/vehicle-required", controllers.BookingVehicleRequiredEscalate)
|
|
milerAuth.Post("/bookings/:bookingid/cancel", controllers.MilerCancelAssignment)
|
|
// Pre-pickup skip: defer a not-yet-picked-up booking without releasing it,
|
|
// so the rider can resume it (the consignment skip needs a consignment).
|
|
milerAuth.Post("/bookings/:bookingid/skip", controllers.MilerSkipPickup)
|
|
|
|
// 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)
|
|
|
|
// Proof-of-delivery / signature upload: hands the app a short-lived presigned
|
|
// PUT URL so the Spaces credentials stay server-side (the legacy rider app
|
|
// shipped the bucket key). App PUTs the image, then sends back the returned
|
|
// public URL as photourl / receiversignatureurl on deliver.
|
|
milerAuth.Post("/uploads/sign", controllers.MilerSignUpload)
|
|
|
|
// Consignment current-state read: lets the app know whether a consignment is
|
|
// collected / out-for-delivery / delivered without replaying the logs history.
|
|
milerAuth.Get("/consignments/:consignmentid", controllers.MilerGetConsignment)
|
|
|
|
// Delivery confirmation
|
|
// start-delivery moves a collected hyperlocal parcel to out-for-delivery (and
|
|
// issues the receiver OTP); deliver/skip are the terminal/failed outcomes.
|
|
milerAuth.Post("/consignments/:id/start-delivery", middlewares.Idempotency(), controllers.MilerStartDelivery)
|
|
milerAuth.Post("/consignments/:id/deliver", middlewares.Idempotency(), controllers.MilerDeliverConsignment)
|
|
milerAuth.Post("/consignments/:id/skip", controllers.MilerSkipDelivery)
|
|
// Rider handover at a base. The authoritative record that a hub-routed parcel
|
|
// physically changed hands; answers with the resulting state rather than a
|
|
// bare 200, and carries the shared idempotency middleware because riders retry
|
|
// on bad signal at a loading bay.
|
|
milerAuth.Post("/consignments/:id/inward-at-hub", middlewares.Idempotency(), controllers.MilerInwardConsignmentAtHub)
|
|
|
|
// Base master data on a rider token — id, name, address, pincode and
|
|
// coordinates for every active base. /admin/tenants/:id/locations is a
|
|
// different dataset (a client's own sites) and is closed to role 5 by design.
|
|
milerAuth.Get("/bases", controllers.MilerGetBases)
|
|
|
|
// 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)
|
|
|
|
// Client onboarding: a tenant + its console login in one transaction. Only
|
|
// the CLIENT_ONBOARDING_OWNERS logins (default admin@doormile.com), and
|
|
// only as Doormile staff with roleid 1 — onboarding mints credentials.
|
|
onboarding := adminAuth.Group("/clients", middlewares.ClientOnboardingOwnerOnly(cfg.ClientOnboardingOwners))
|
|
onboarding.Post("/onboard", controllers.OnboardClient)
|
|
onboarding.Get("/onboarded", controllers.GetOnboardedClients)
|
|
onboarding.Get("/cities", controllers.GetOnboardingCities)
|
|
// Edit and remove a client's console login (:id = doormile_auth id). Remove
|
|
// deletes the login only and marks the client Inactive; history is kept.
|
|
onboarding.Put("/:id", controllers.UpdateOnboardedClient)
|
|
onboarding.Delete("/:id", controllers.DeleteOnboardedClient)
|
|
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.Get("/customers/summary", controllers.GetAdminCustomersSummary)
|
|
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)
|
|
// Manual trigger: hand all pending express orders for the tenant to the
|
|
// ExpressDispatchAgent (assign + road-sequence). Operator-controlled, after
|
|
// orders have accumulated batch by batch.
|
|
adminAuth.Post("/expressbooking/dispatch", controllers.DispatchExpressBatch)
|
|
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)
|
|
|
|
// AI agent registry (krow_talent_app/docs/agent-platform-plan.md, Phase 1).
|
|
// Doormile staff only — a partner-tenant login gets 403. Reads are open to
|
|
// roles 1/3/4; every write is roleid 1 only and is audited.
|
|
aiRegistry := adminAuth.Group("/ai", middlewares.DoormileStaffOnly)
|
|
aiRegistry.Get("/agents", controllers.GetAIAgents)
|
|
aiRegistry.Get("/agents/:id", controllers.GetAIAgent)
|
|
aiRegistry.Patch("/agents/:id", middlewares.RoleCheckMiddleware(1), controllers.PatchAIAgent)
|
|
aiRegistry.Get("/skills", controllers.GetAISkills)
|
|
aiRegistry.Post("/skills", middlewares.RoleCheckMiddleware(1), controllers.CreateAISkill)
|
|
aiRegistry.Patch("/skills/:id", middlewares.RoleCheckMiddleware(1), controllers.PatchAISkill)
|
|
aiRegistry.Get("/tools", controllers.GetAITools)
|
|
aiRegistry.Get("/audit", controllers.GetAIRegistryAudit)
|
|
// What the agents did (Phase 4): runs from AI_engine telemetry, decisions
|
|
// from agent_decisions, live heartbeat from Redis. Read-only.
|
|
aiRegistry.Get("/insights", controllers.GetAIInsights)
|
|
aiRegistry.Get("/decisions", controllers.GetAIDecisions)
|
|
// Test tab (Phase 6): one prompt through Claude with a skill's tools. Reads
|
|
// run redacted; writes only become proposals. Admin only — every run is a
|
|
// paid API call — and rate-limited per user in the handler.
|
|
aiRegistry.Post("/playground/run", middlewares.RoleCheckMiddleware(1), controllers.RunAIPlayground)
|
|
|
|
// --------------------
|
|
// 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)
|
|
// What riders are carrying towards this base but have not handed over yet,
|
|
// and the matching receive/dispute action for when they arrive.
|
|
hubAuth.Get("/inbound/expected", controllers.GetHubInboundExpected)
|
|
hubAuth.Post("/inbound/:id/reconcile", controllers.ReconcileHubInbound)
|
|
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)
|
|
// The agent registry, for AI_engine to poll (ETag / If-None-Match → 304).
|
|
internal.Get("/ai/registry", controllers.GetInternalAIRegistry)
|
|
|
|
// Express-batch dispatch: the ExpressDispatchAgent reads a tenant's riders
|
|
// and the batch's bookings, then writes back the assignments it decided.
|
|
internal.Get("/express/riders", controllers.GetExpressRiders)
|
|
internal.Get("/express/bookings", controllers.GetExpressBookings)
|
|
internal.Post("/express/assign", controllers.AssignExpressBatch)
|
|
|
|
// --------------------
|
|
// 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))
|
|
}
|