fix: panic recovery, rate limiting, transaction error handling, pagination

Hardening pass over the API surface. No route's auth requirements change.

Resilience:
- Add recover middleware. There was none, so an unhandled panic in any
  handler propagated out of the process instead of becoming a 500.
- Add a centralized ErrorHandler so errors and recovered panics return the
  same {success,message} envelope as the utils helpers, not Fiber's default
  plain-text body. 5xx responses are logged with method and path.

Rate limiting:
- Global 300/min per IP as an abuse backstop, exempting health/readiness
  probes and websocket upgrades.
- 10/min shared across every credential endpoint (customer/miler/admin/hub
  login, verify-pin, reset-pin, email OTP). PINs are 4 digits, so the whole
  keyspace was previously walkable in seconds. One shared limiter instance
  means rotating between endpoints doesn't reset the budget.
- Add TRUSTED_PROXIES config. Limits key on c.IP(), which behind a TLS
  terminator is the proxy, collapsing every client into one bucket. When set,
  X-Forwarded-For is honoured only from those proxies so the header can't be
  spoofed to dodge the limit. Logs a warning when unset.

Transactions:
- Check the error on all 51 previously-unchecked tx.Save/Create/Delete/
  Model(...).Update/Commit calls across 6 controllers. A failed write inside
  a transaction was silently ignored and the request still reported success;
  an unchecked Commit could fail with the caller told everything worked.
  Each site now rolls back and returns a specific message.

Pagination:
- Add utils.ParsePage/Paginated, reusing the pageno/pagesize convention
  GetAdminBookings already established. Default 500, hard cap 1000.
- Apply to the previously unbounded consignments, tripsheets, exceptions,
  app-users and clients endpoints. Defaults are high so existing consoles
  that don't paginate keep working; the cap only stops a growing table from
  being loaded wholesale. total is now a real COUNT, not len(data).
- GetClients also loaded the entire auth table to join in memory; it now
  fetches only the current page's rows.

Tests (first in the repo):
- Extract the hyperlocal pincode rule out of BookingPickupComplete into
  isHyperlocal so it is testable, covering the short/empty pincode fallback.
- Cover calculateVolumetricWeight and the ParsePage clamping rules.

Repo hygiene:
- Tag scratch/*.go with //go:build ignore. Each declared its own main(), so
  `go build ./...` failed on redeclaration; it now passes repo-wide.
- Untrack scratch/node_modules (216 files) and ignore node_modules, test
  artifacts, and the `doormile` binary `go build .` emits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-07-27 12:13:39 +05:30
parent a2b9268189
commit 2c26cbe4ba
240 changed files with 753 additions and 76753 deletions

View File

@@ -336,19 +336,31 @@ func AcceptMilerAssignment(c *fiber.Ctx) error {
now := time.Now()
assignment.Assignmentstatus = constants.AssignmentAccepted
assignment.Acceptedat = &now
tx.Save(&assignment)
if err := tx.Save(&assignment).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to accept assignment")
}
var booking models.PickupBooking
if err := tx.First(&booking, assignment.Bookingid).Error; err == nil {
booking.Status = constants.BookingPickupScheduled
booking.Assignedmileruserid = &milerUserID
booking.Updatedat = now
tx.Save(&booking)
if err := tx.Save(&booking).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update booking")
}
}
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAssigned)
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
Update("availabilitystatus", constants.MilerAssigned).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler availability")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to commit assignment acceptance")
}
if booking.Bookingid != 0 {
var customer models.AppCustomer
@@ -394,19 +406,31 @@ func RejectMilerAssignment(c *fiber.Ctx) error {
ba.Assignmentstatus = constants.AssignmentRejected
ba.Remarks = req.Reason
tx.Save(&ba)
if err := tx.Save(&ba).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to reject assignment")
}
var booking models.PickupBooking
if err := tx.First(&booking, ba.Bookingid).Error; err == nil {
booking.Status = constants.BookingCreated
booking.Assignedmileruserid = nil
booking.Updatedat = time.Now()
tx.Save(&booking)
if err := tx.Save(&booking).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to release booking")
}
}
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler availability")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to commit assignment rejection")
}
if booking.Bookingid != 0 {
if booking.Bookingsource == "CRM_Console" {
@@ -434,9 +458,15 @@ func BookingReachedCustomer(c *fiber.Ctx) error {
return utils.NotFound(c, "assigned booking not found")
}
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAtCustomer)
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
Update("availabilitystatus", constants.MilerAtCustomer).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler availability")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to confirm arrival")
}
return utils.Message(c, "arrival at customer confirmed")
}
@@ -500,7 +530,7 @@ func BookingParcelConfirm(c *fiber.Ctx) error {
}
return utils.OK(c, fiber.Map{
"parcels": parcels,
"parcels": parcels,
"total_chargeable_weight": totalChargeable,
})
}
@@ -544,6 +574,19 @@ func BookingPaymentCollect(c *fiber.Ctx) error {
return utils.Created(c, payment)
}
// isHyperlocal reports whether a pickup and delivery pincode fall in the same
// 3-digit postal area, following the same zone-prefix convention as
// hubPincodePrefix in hubController.go. A same-area booking needs no
// hub-to-hub tripsheet leg, so the collecting miler can carry it straight to
// final-mile delivery. Pincodes shorter than 3 characters are treated as
// unknown rather than matching, so bad data falls back to the safe hub route.
func isHyperlocal(pickupPincode, deliveryPincode string) bool {
if len(pickupPincode) < 3 || len(deliveryPincode) < 3 {
return false
}
return pickupPincode[:3] == deliveryPincode[:3]
}
func BookingPickupComplete(c *fiber.Ctx) error {
milerUserID := c.Locals("userid").(int)
bookingID, err := strconv.Atoi(c.Params("bookingid"))
@@ -562,14 +605,20 @@ func BookingPickupComplete(c *fiber.Ctx) error {
now := time.Now()
booking.Status = constants.BookingPickedUp
booking.Updatedat = now
tx.Save(&booking)
if err := tx.Save(&booking).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update booking status")
}
var profile models.MilerProfile
if err := tx.Where("userid = ?", milerUserID).First(&profile).Error; err == nil {
profile.Totalcompletedpickups += 1
profile.Availabilitystatus = constants.MilerPickedUp
profile.Updatedat = now
tx.Save(&profile)
if err := tx.Save(&profile).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler profile")
}
}
var parcels []models.BookingParcel
@@ -612,8 +661,7 @@ func BookingPickupComplete(c *fiber.Ctx) error {
// no hub-to-hub tripsheet leg is needed, so the same miler goes straight
// to final-mile delivery instead of parking the consignment at the hub.
consignmentStatus := constants.ConsignmentInwardedAtHub
if len(booking.Pickuppincode) >= 3 && len(booking.Deliverypincode) >= 3 &&
booking.Pickuppincode[:3] == booking.Deliverypincode[:3] {
if isHyperlocal(booking.Pickuppincode, booking.Deliverypincode) {
consignmentStatus = constants.ConsignmentOutForDelivery
}
@@ -657,7 +705,10 @@ func BookingPickupComplete(c *fiber.Ctx) error {
booking.Consignmentid = &consignment.Consignmentid
booking.Status = constants.BookingConvertedConsignment
tx.Save(&booking)
if err := tx.Save(&booking).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to link booking to consignment")
}
history := models.ConsignmentHistory{
Consignmentid: consignment.Consignmentid,
@@ -666,11 +717,20 @@ func BookingPickupComplete(c *fiber.Ctx) error {
Eventstatus: consignmentStatus,
Remarks: "Package collected by miler and converted to consignment",
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler availability")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to complete pickup")
}
var customer models.AppCustomer
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
@@ -936,7 +996,9 @@ func PublishConsignmentLogs(c *fiber.Ctx) error {
return utils.Internal(c, "failed to publish logs to cache")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to publish consignment logs")
}
return utils.Message(c, "consignment logs published successfully")
}