Security: add rate limiting, input validation, and filter category

validation
Backend:
- Add rate limiting middleware (mw/ratelimit.go) - in-memory per-IP
  limiter
- Apply rate limits per endpoint group:
  - Public read-only: 120/min
  - Registration: 10/min
  - Portfolio filters: 60/min
  - Authenticated users: 120/min
  - Admin: none (trusted)
- Add 256 char input length validation on portfolio endpoints
- Validate filter categories exist in DB before querying
- Secure GetImage endpoint: only allow UUID or numeric timestamp (15-20
  digits)
- Remove pattern-based image lookup to prevent enumeration
- Add services validation: name (100), duration (1-480), patch test
  (0-168)
  Frontend:
- Add maxlength=256 to portfolio tag/search inputs
- Add maxlength to registration: name (50), email (255), phone (20),
  password (72)
- Add maxlength=100 to service name input
This commit is contained in:
2026-02-20 12:03:14 +00:00
parent f9eec94f2f
commit a5a2ffd83e
9 changed files with 153 additions and 31 deletions
+23 -19
View File
@@ -81,38 +81,46 @@ func main() {
// All API routes grouped under /api for clarity
r.Route("/api", func(r chi.Router) {
// --- Public Routes ---
r.Get("/services", services.ServicesHandler)
r.Post("/register", authHandlers.RegisterHandler)
// Public read-only
r.Group(func(r chi.Router) {
r.Use(mw.RateLimit(120, time.Minute))
r.Get("/services", services.ServicesHandler)
})
// Registration: 10/min to prevent spam
r.With(mw.RateLimit(10, time.Minute)).Post("/register", authHandlers.RegisterHandler)
// Login: Has its own internal rate limiting
r.Post("/login", authHandlers.LoginHandler)
// --- Portfolio Routes (public read, admin write) ---
// Portfolio
r.Route("/portfolio", func(r chi.Router) {
r.Get("/images", portfolio.ListImages)
r.Get("/tags", portfolio.ListTags)
r.Get("/filters", portfolio.ListFilters)
r.Get("/images/{id}", portfolio.GetImage)
r.With(mw.RateLimit(60, time.Minute)).Get("/filters", portfolio.ListFilters)
r.With(mw.RateLimit(120, time.Minute)).Get("/images/{id}", portfolio.GetImage)
r.Group(func(r chi.Router) {
r.Use(mw.RequireAuth)
r.Use(mw.RequireAdmin)
r.Use(mw.RateLimit(60, time.Minute))
r.Post("/images", portfolio.UploadImage)
r.Delete("/images/{id}", portfolio.DeleteImage)
})
})
// --- Scheduling Routes ---
// Scheduling
r.Route("/scheduling", func(r chi.Router) {
// Public GET routes
r.Use(mw.RateLimit(120, time.Minute))
r.Get("/default-hours", scheduling.GetDefaultHours)
r.Get("/exceptional-groups", scheduling.ListExceptionalGroups)
r.Get("/working-hours", scheduling.GetWorkingHours)
r.Get("/available-hours", scheduling.GetAvailableHours)
// Admin-only scheduling modifications
r.Group(func(r chi.Router) {
r.Use(mw.RequireAuth)
r.Use(mw.RequireAdmin)
r.Use(mw.RateLimit(60, time.Minute))
r.Put("/default-hours", scheduling.UpdateDefaultHours)
r.Post("/exceptional-groups", scheduling.CreateExceptionalGroup)
@@ -121,9 +129,10 @@ func main() {
})
})
// --- Protected routes (any authenticated user) ---
// Authenticated users
r.Group(func(r chi.Router) {
r.Use(mw.RequireAuth)
r.Use(mw.RateLimit(120, time.Minute))
r.Post("/refresh-token", authHandlers.RefreshTokenHandler)
@@ -132,17 +141,16 @@ func main() {
r.Delete("/user/account", user.DeleteAccountHandler)
r.Get("/user/loyalty", user.GetLoyaltyHandler)
// Booking routes for authenticated users
r.Route("/bookings", func(r chi.Router) {
r.Get("/", bookings.GetAllUserBookingsHandler)
r.Post("/", bookings.CreateBookingHandler)
r.Get("/", bookings.GetAllUserBookingsHandler)
r.Get("/{id}", bookings.GetBookingHandler)
r.Put("/{id}", bookings.EditBookingHandler)
r.Delete("/{id}", bookings.DeleteBookingHandler)
})
})
// --- Admin-only routes ---
// Admin-only (no rate limit - trusted users with authenticated sessions)
r.Group(func(r chi.Router) {
r.Use(mw.RequireAuth)
r.Use(mw.RequireAdmin)
@@ -157,11 +165,11 @@ func main() {
r.Route("/admin/bookings", func(r chi.Router) {
r.Get("/", bookings.GetAllAdminBookingsHandler)
r.Post("/", bookings.AdminCreateBookingForUserHandler)
r.Get("/search", bookings.SearchAdminBookingsHandler)
r.With(mw.RateLimit(60, time.Minute)).Get("/search", bookings.SearchAdminBookingsHandler)
r.Get("/user/{user_id}", bookings.GetAllBookingsByUserHandler)
r.Get("/{id}", bookings.GetAdminBookingHandler)
r.Put("/{id}/progress", bookings.ProgressBookingHandler)
r.Post("/{id}/confirm", bookings.ConfirmBookingHandler) // HERE
r.Post("/{id}/confirm", bookings.ConfirmBookingHandler)
r.Post("/{id}/cancel", bookings.ConfirmBookingHandler)
})
@@ -176,12 +184,8 @@ func main() {
r.Get("/pending-approvals", today.GetPendingApprovalsHandler)
})
// --- Admin Notifications ---
r.Route("/admin/notifications", func(r chi.Router) {
// List all unacknowledged notifications
r.Get("/", notifications.GetNotifications)
// Acknowledge a single notification
r.Post("/{id}/acknowledge", notifications.AcknowledgeNotification)
})
})