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:
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user