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:
@@ -11,12 +11,33 @@ import (
|
||||
"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
|
||||
// --------------------
|
||||
@@ -61,11 +82,11 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
// --------------------
|
||||
customer := api.Group("/customer")
|
||||
customer.Post("/register", controllers.RegisterCustomer(cfg))
|
||||
customer.Post("/login", controllers.LoginCustomer)
|
||||
customer.Post("/verify-pin", controllers.VerifyCustomerPin(cfg))
|
||||
customer.Post("/reset-pin", controllers.ResetCustomerPin)
|
||||
customer.Post("/send-email-otp", controllers.SendCustomerEmailOtp(cfg))
|
||||
customer.Post("/verify-email-otp", controllers.VerifyCustomerEmailOtp())
|
||||
customer.Post("/login", authThrottle, controllers.LoginCustomer)
|
||||
customer.Post("/verify-pin", authThrottle, controllers.VerifyCustomerPin(cfg))
|
||||
customer.Post("/reset-pin", authThrottle, controllers.ResetCustomerPin)
|
||||
customer.Post("/send-email-otp", authThrottle, controllers.SendCustomerEmailOtp(cfg))
|
||||
customer.Post("/verify-email-otp", authThrottle, controllers.VerifyCustomerEmailOtp())
|
||||
|
||||
// Authenticated Customer App routes
|
||||
customerAuth := customer.Use(middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(9))
|
||||
@@ -90,8 +111,8 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
// MILER APIS
|
||||
// --------------------
|
||||
miler := api.Group("/miler")
|
||||
miler.Post("/login", controllers.LoginMiler(cfg))
|
||||
miler.Post("/verify-pin", controllers.VerifyMilerPin(cfg))
|
||||
miler.Post("/login", authThrottle, controllers.LoginMiler(cfg))
|
||||
miler.Post("/verify-pin", authThrottle, controllers.VerifyMilerPin(cfg))
|
||||
|
||||
// Authenticated Miler App routes
|
||||
milerAuth := miler.Use(middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(5))
|
||||
@@ -156,7 +177,7 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
// --------------------
|
||||
admin := api.Group("/admin")
|
||||
|
||||
admin.Post("/login", controllers.LoginAdmin(cfg))
|
||||
admin.Post("/login", authThrottle, controllers.LoginAdmin(cfg))
|
||||
|
||||
// Authenticated Admin Console routes
|
||||
adminAuth := admin.Use(middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(1, 3, 4))
|
||||
@@ -274,7 +295,7 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
// HUB CONSOLE APIS
|
||||
// --------------------
|
||||
hub := api.Group("/hub")
|
||||
hub.Post("/login", controllers.HubStaffLogin(cfg))
|
||||
hub.Post("/login", authThrottle, controllers.HubStaffLogin(cfg))
|
||||
|
||||
// Authenticated Hub Console routes (role 6)
|
||||
hubAuth := hub.Use(middlewares.HubStaffAuth(cfg))
|
||||
|
||||
Reference in New Issue
Block a user