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:
@@ -119,20 +119,39 @@ func RegisterClient(c *fiber.Ctx) error {
|
||||
role = auth.Role
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return utils.Internal(c, "failed to register client")
|
||||
}
|
||||
|
||||
return utils.Created(c, buildClientResponse(client, email, role))
|
||||
}
|
||||
|
||||
func GetClients(c *fiber.Ctx) error {
|
||||
page := utils.ParsePage(c)
|
||||
|
||||
var total int64
|
||||
if err := db.DB.Model(&models.DoormileClient{}).Count(&total).Error; err != nil {
|
||||
return utils.Internal(c, "failed to count clients")
|
||||
}
|
||||
|
||||
var clients []models.DoormileClient
|
||||
if err := db.DB.Find(&clients).Error; err != nil {
|
||||
if err := page.Apply(db.DB).Find(&clients).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch clients")
|
||||
}
|
||||
|
||||
// Bulk load auth records once and map by client_id for O(1) lookup
|
||||
// Bulk load auth records for just this page's clients, rather than the
|
||||
// whole auth table, and map by client_id for O(1) lookup.
|
||||
clientIDs := make([]uint64, 0, len(clients))
|
||||
for _, client := range clients {
|
||||
clientIDs = append(clientIDs, client.ID)
|
||||
}
|
||||
|
||||
var auths []models.DoormileAuth
|
||||
db.DB.Find(&auths)
|
||||
if len(clientIDs) > 0 {
|
||||
if err := db.DB.Where("client_id IN ?", clientIDs).Find(&auths).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch client credentials")
|
||||
}
|
||||
}
|
||||
authByClientID := make(map[uint64]models.DoormileAuth, len(auths))
|
||||
for _, a := range auths {
|
||||
if a.ClientID != nil {
|
||||
@@ -146,7 +165,7 @@ func GetClients(c *fiber.Ctx) error {
|
||||
responses = append(responses, buildClientResponse(client, auth.Email, auth.Role))
|
||||
}
|
||||
|
||||
return utils.List(c, responses, int64(len(responses)))
|
||||
return utils.Paginated(c, responses, total, page)
|
||||
}
|
||||
|
||||
func GetClientDetails(c *fiber.Ctx) error {
|
||||
@@ -304,7 +323,9 @@ func UpdateClient(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return utils.Internal(c, "failed to update client")
|
||||
}
|
||||
|
||||
return utils.OK(c, buildClientResponse(client, email, role))
|
||||
}
|
||||
@@ -332,7 +353,9 @@ func DeleteClient(c *fiber.Ctx) error {
|
||||
return utils.Internal(c, "failed to delete client")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return utils.Internal(c, "failed to delete client")
|
||||
}
|
||||
|
||||
return utils.Message(c, "client deleted successfully")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user