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

@@ -117,9 +117,16 @@ func GetAdminDashboard(c *fiber.Ctx) error {
// --------------------
func GetAppUsers(c *fiber.Ctx) error {
page := utils.ParsePage(c)
var total int64
if err := db.DB.Model(&models.AppUser{}).Where("roleid != ?", 5).Count(&total).Error; err != nil {
return utils.Internal(c, "failed to count users")
}
var users []models.AppUser
// Exclude Milers (Roleid = 5) from the CRM user list
if err := db.DB.Where("roleid != ?", 5).Find(&users).Error; err != nil {
if err := page.Apply(db.DB.Where("roleid != ?", 5)).Find(&users).Error; err != nil {
return utils.Internal(c, "failed to fetch users")
}
@@ -145,7 +152,7 @@ func GetAppUsers(c *fiber.Ctx) error {
"status": u.Status,
})
}
return utils.List(c, response, int64(len(response)))
return utils.Paginated(c, response, total, page)
}
func CreateAppUser(c *fiber.Ctx) error {
@@ -935,7 +942,9 @@ func CreateMiler(c *fiber.Ctx) error {
return utils.Internal(c, "failed to create miler profile")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to create miler")
}
return utils.Created(c, profile)
}
@@ -995,11 +1004,20 @@ func BlockMiler(c *fiber.Ctx) error {
profile.Availabilitystatus = constants.MilerBlocked
profile.Updatedat = time.Now()
tx.Save(&profile)
if err := tx.Save(&profile).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to block miler profile")
}
tx.Model(&models.AppUser{}).Where("userid = ?", profile.Userid).Update("status", "Blocked")
if err := tx.Model(&models.AppUser{}).Where("userid = ?", profile.Userid).
Update("status", "Blocked").Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to block miler account")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to block miler")
}
return utils.Message(c, "miler blocked successfully")
}
@@ -1060,27 +1078,27 @@ func GetAdminBookings(c *fiber.Ctx) error {
func CreateCRMBooking(c *fiber.Ctx) error {
type AdminBookingRequest struct {
Appcustomerid int `json:"appcustomerid"`
CustomerPhone string `json:"customer_phone"`
CustomerName string `json:"customer_name"`
Pickupaddress string `json:"pickupaddress"`
Pickuppincode string `json:"pickuppincode"`
Pickuplatitude float64 `json:"pickuplatitude"`
Pickuplongitude float64 `json:"pickuplongitude"`
Deliveryaddress string `json:"deliveryaddress"`
Deliverypincode string `json:"deliverypincode"`
Deliverycity string `json:"deliverycity"`
Deliverylatitude float64 `json:"deliverylatitude"`
Deliverylongitude float64 `json:"deliverylongitude"`
Providercompany string `json:"providercompany"`
Providerlocation string `json:"providerlocation"`
Notes string `json:"notes"`
ServiceOption string `json:"service_option"`
Finalprice float64 `json:"finalprice"`
Insuranceamount float64 `json:"insuranceamount"`
Preferredpickupfrom *time.Time `json:"preferredpickupfrom"`
Preferredpickupto *time.Time `json:"preferredpickupto"`
Parcels []dto.ParcelRequest `json:"parcels"`
Appcustomerid int `json:"appcustomerid"`
CustomerPhone string `json:"customer_phone"`
CustomerName string `json:"customer_name"`
Pickupaddress string `json:"pickupaddress"`
Pickuppincode string `json:"pickuppincode"`
Pickuplatitude float64 `json:"pickuplatitude"`
Pickuplongitude float64 `json:"pickuplongitude"`
Deliveryaddress string `json:"deliveryaddress"`
Deliverypincode string `json:"deliverypincode"`
Deliverycity string `json:"deliverycity"`
Deliverylatitude float64 `json:"deliverylatitude"`
Deliverylongitude float64 `json:"deliverylongitude"`
Providercompany string `json:"providercompany"`
Providerlocation string `json:"providerlocation"`
Notes string `json:"notes"`
ServiceOption string `json:"service_option"`
Finalprice float64 `json:"finalprice"`
Insuranceamount float64 `json:"insuranceamount"`
Preferredpickupfrom *time.Time `json:"preferredpickupfrom"`
Preferredpickupto *time.Time `json:"preferredpickupto"`
Parcels []dto.ParcelRequest `json:"parcels"`
}
req := new(AdminBookingRequest)
@@ -1178,7 +1196,7 @@ func CreateCRMBooking(c *fiber.Ctx) error {
if p.Needsinsurance {
parcel.Insuranceamount = p.Declaredvalue * 0.01
}
// If explicit insurance amount is provided from CRM, apply it to the first parcel
if req.Insuranceamount > 0 && totalWeight == math.Max(p.Weight, calculateVolumetricWeight(p.Length, p.Width, p.Height)) {
parcel.Insuranceamount = req.Insuranceamount
@@ -1195,10 +1213,10 @@ func CreateCRMBooking(c *fiber.Ctx) error {
if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 {
distance = calculateDistance(booking.Pickuplatitude, booking.Pickuplongitude, booking.Deliverylatitude, booking.Deliverylongitude)
}
var pricing models.Pricing
err := tx.Where("status = ? AND ? BETWEEN effectivefrom AND effectiveto", "Active", time.Now()).Order("priority DESC").First(&pricing).Error
var estimatedPrice float64
var pricingID *int
if err == nil {
@@ -1257,22 +1275,27 @@ func CreateCRMBooking(c *fiber.Ctx) error {
Reason: "Oversized package / heavy weight",
Status: "Required",
}
tx.Create(&reqVeh)
if err := tx.Create(&reqVeh).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to save vehicle requirement")
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to create booking")
}
go assignment.AssignCRMMiler(booking.Bookingid)
if db.Js != nil {
payload := map[string]interface{}{
"booking_id": booking.Bookingid,
"booking_no": booking.Bookingno,
"customer_id": booking.Appcustomerid,
"pickup_address": booking.Pickupaddress,
"pickup_pincode": booking.Pickuppincode,
"status": constants.BookingPendingPickup,
"created_at": time.Now().UnixMilli(),
"booking_id": booking.Bookingid,
"booking_no": booking.Bookingno,
"customer_id": booking.Appcustomerid,
"pickup_address": booking.Pickupaddress,
"pickup_pincode": booking.Pickuppincode,
"status": constants.BookingPendingPickup,
"created_at": time.Now().UnixMilli(),
}
if data, err := json.Marshal(payload); err == nil {
db.Js.Publish("api.v1.bookings.create", data)
@@ -1425,11 +1448,18 @@ func AdminCancelBooking(c *fiber.Ctx) error {
// --------------------
func GetAdminConsignments(c *fiber.Ctx) error {
page := utils.ParsePage(c)
var total int64
if err := db.DB.Model(&models.Consignment{}).Count(&total).Error; err != nil {
return utils.Internal(c, "failed to count consignments")
}
var list []models.Consignment
if err := db.DB.Find(&list).Error; err != nil {
if err := page.Apply(db.DB).Find(&list).Error; err != nil {
return utils.Internal(c, "failed to fetch consignments")
}
return utils.List(c, list, int64(len(list)))
return utils.Paginated(c, list, total, page)
}
func GetAdminConsignmentDetails(c *fiber.Ctx) error {
@@ -1478,7 +1508,10 @@ func AdminUpdateConsignmentStatus(c *fiber.Ctx) error {
consignment.Status = req.Status
consignment.Updatedat = time.Now()
tx.Save(&consignment)
if err := tx.Save(&consignment).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update consignment status")
}
adminUserID := c.Locals("userid").(int)
@@ -1488,9 +1521,14 @@ func AdminUpdateConsignmentStatus(c *fiber.Ctx) error {
Eventstatus: req.Status,
Remarks: req.Remarks,
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to update consignment")
}
return utils.OK(c, consignment)
}
@@ -1499,11 +1537,18 @@ func AdminUpdateConsignmentStatus(c *fiber.Ctx) error {
// --------------------
func GetTripsheets(c *fiber.Ctx) error {
page := utils.ParsePage(c)
var total int64
if err := db.DB.Model(&models.Tripsheet{}).Where("deletedat IS NULL").Count(&total).Error; err != nil {
return utils.Internal(c, "failed to count tripsheets")
}
var list []models.Tripsheet
if err := db.DB.Where("deletedat IS NULL").Find(&list).Error; err != nil {
if err := page.Apply(db.DB.Where("deletedat IS NULL")).Find(&list).Error; err != nil {
return utils.Internal(c, "failed to fetch tripsheets")
}
return utils.List(c, list, int64(len(list)))
return utils.Paginated(c, list, total, page)
}
func CreateTripsheet(c *fiber.Ctx) error {
@@ -1592,26 +1637,38 @@ func DispatchTripsheet(c *fiber.Ctx) error {
tripsheet.Status = constants.TripsheetDispatched
tripsheet.Dispatchtime = &now
tripsheet.Updatedat = now
tx.Save(&tripsheet)
if err := tx.Save(&tripsheet).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to dispatch tripsheet")
}
// Fetch all loaded items
var items []models.TripsheetItem
tx.Where("tripsheetid = ?", id).Find(&items)
if err := tx.Where("tripsheetid = ?", id).Find(&items).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to load tripsheet items")
}
adminUserID := c.Locals("userid").(int)
for _, item := range items {
// Update item scanning
tx.Model(&item).Updates(map[string]interface{}{
if err := tx.Model(&item).Updates(map[string]interface{}{
"scanstatus": constants.ScanLoaded,
"scannedat": &now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update tripsheet item scan status")
}
// Update consignment status to In_Transit
tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{
if err := tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{
"status": constants.ConsignmentInTransit,
"updatedat": now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update consignment status")
}
// Log history
history := models.ConsignmentHistory{
@@ -1621,10 +1678,15 @@ func DispatchTripsheet(c *fiber.Ctx) error {
Eventstatus: constants.ConsignmentInTransit,
Remarks: fmt.Sprintf("Consignment dispatched on Tripsheet %s", tripsheet.Tripsheetno),
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to dispatch tripsheet")
}
return utils.OK(c, tripsheet)
}
@@ -1642,26 +1704,38 @@ func ArriveTripsheet(c *fiber.Ctx) error {
tripsheet.Status = constants.TripsheetArrived
tripsheet.Arrivaltime = &now
tripsheet.Updatedat = now
tx.Save(&tripsheet)
if err := tx.Save(&tripsheet).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to mark tripsheet arrived")
}
// Fetch items
var items []models.TripsheetItem
tx.Where("tripsheetid = ?", id).Find(&items)
if err := tx.Where("tripsheetid = ?", id).Find(&items).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to load tripsheet items")
}
adminUserID := c.Locals("userid").(int)
for _, item := range items {
tx.Model(&item).Updates(map[string]interface{}{
if err := tx.Model(&item).Updates(map[string]interface{}{
"scanstatus": constants.ScanUnloaded,
"scannedat": &now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update tripsheet item scan status")
}
// Update consignment status back to Inwarded_at_Hub at destination
tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{
if err := tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{
"status": constants.ConsignmentInwardedAtHub,
"currenthubid": tripsheet.Destinationhubid,
"updatedat": now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update consignment status")
}
// Log history
history := models.ConsignmentHistory{
@@ -1672,10 +1746,15 @@ func ArriveTripsheet(c *fiber.Ctx) error {
Eventstatus: constants.ConsignmentInwardedAtHub,
Remarks: fmt.Sprintf("Consignment arrived at Hub on Tripsheet %s", tripsheet.Tripsheetno),
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to mark tripsheet arrived")
}
return utils.OK(c, tripsheet)
}
@@ -1796,7 +1875,7 @@ func GetPricingQuoteSimulate(c *fiber.Ctx) error {
}
distance := calculateDistance(req.Pickuplatitude, req.Pickuplongitude, req.Deliverylatitude, req.Deliverylongitude)
var totalWeight float64
for _, p := range req.Parcels {
volumetric := calculateVolumetricWeight(p.Length, p.Width, p.Height)
@@ -1831,11 +1910,18 @@ func GetPricingQuoteSimulate(c *fiber.Ctx) error {
// --------------------
func GetExceptions(c *fiber.Ctx) error {
page := utils.ParsePage(c)
var total int64
if err := db.DB.Model(&models.ConsignmentException{}).Where("deletedat IS NULL").Count(&total).Error; err != nil {
return utils.Internal(c, "failed to count exceptions")
}
var list []models.ConsignmentException
if err := db.DB.Where("deletedat IS NULL").Find(&list).Error; err != nil {
if err := page.Apply(db.DB.Where("deletedat IS NULL")).Find(&list).Error; err != nil {
return utils.Internal(c, "failed to fetch exceptions")
}
return utils.List(c, list, int64(len(list)))
return utils.Paginated(c, list, total, page)
}
func CreateException(c *fiber.Ctx) error {
@@ -1878,8 +1964,12 @@ func CreateException(c *fiber.Ctx) error {
}
if cStatus != "" {
tx.Model(&models.Consignment{}).Where("consignmentid = ?", req.Consignmentid).Update("status", cStatus)
if err := tx.Model(&models.Consignment{}).Where("consignmentid = ?", req.Consignmentid).
Update("status", cStatus).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update consignment status")
}
// Log history
history := models.ConsignmentHistory{
Consignmentid: req.Consignmentid,
@@ -1889,10 +1979,15 @@ func CreateException(c *fiber.Ctx) error {
Eventstatus: cStatus,
Remarks: fmt.Sprintf("Exception reported: %s. Description: %s", req.Exceptiontype, req.Description),
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to record exception")
}
return utils.Created(c, exception)
}
@@ -2231,18 +2326,24 @@ func InternalReassign(c *fiber.Ctx) error {
now := time.Now()
if booking.Assignedmileruserid != nil {
tx.Model(&models.MilerProfile{}).
if err := tx.Model(&models.MilerProfile{}).
Where("userid = ?", *booking.Assignedmileruserid).
Updates(map[string]interface{}{
"availabilitystatus": constants.MilerAvailable,
"updatedat": now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to free previous miler")
}
tx.Delete(&models.BookingAssignment{},
if err := tx.Delete(&models.BookingAssignment{},
"bookingid = ? AND assignmentstatus IN ?",
booking.Bookingid,
[]string{constants.AssignmentAssigned, constants.AssignmentAccepted},
)
).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to clear previous assignment")
}
}
booking.Status = constants.BookingCreated
@@ -2253,7 +2354,9 @@ func InternalReassign(c *fiber.Ctx) error {
return utils.Internal(c, "failed to reset booking")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to reassign booking")
}
if booking.Bookingsource == "CRM_Console" {
go assignment.AssignCRMMiler(booking.Bookingid)