Implement Sprint 2 security enhancements
Add user-based rate limiting middleware (100 req/min) for authenticated endpoints using httprate library. Implement common password validation blocking 50+ weak passwords. Improve test coverage from 38.3% to 44.6% with comprehensive refresh token tests. Security improvements: - Rate limiting by user ID for /habits and /stats endpoints - X-RateLimit-Limit header in responses - Common password blacklist in password validation - Refresh token test suite with 5 scenarios (valid, invalid, expired, revoked, empty)
This commit is contained in:
@@ -13,14 +13,14 @@ import (
|
||||
)
|
||||
|
||||
type AuthHandlers struct {
|
||||
registerHandler *commands.RegisterUserHandler
|
||||
loginHandler *queries.LoginUserHandler
|
||||
refreshTokenHandler *queries.RefreshTokenHandler
|
||||
revokeTokenHandler *commands.RevokeTokenHandler
|
||||
revokeAllTokensHandler *commands.RevokeAllTokensHandler
|
||||
jwtService *auth.JWTService
|
||||
refreshTokenRepo repositories.RefreshTokenRepository
|
||||
refreshTokenExpiry time.Duration
|
||||
registerHandler *commands.RegisterUserHandler
|
||||
loginHandler *queries.LoginUserHandler
|
||||
refreshTokenHandler *queries.RefreshTokenHandler
|
||||
revokeTokenHandler *commands.RevokeTokenHandler
|
||||
revokeAllTokensHandler *commands.RevokeAllTokensHandler
|
||||
jwtService *auth.JWTService
|
||||
refreshTokenRepo repositories.RefreshTokenRepository
|
||||
refreshTokenExpiry time.Duration
|
||||
}
|
||||
|
||||
func NewAuthHandlers(
|
||||
|
||||
@@ -7,15 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type CreateHabitRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type value_objects.HabitType `json:"type"`
|
||||
Frequency value_objects.Frequency `json:"frequency"`
|
||||
SpecificDays []int `json:"specific_days,omitempty"`
|
||||
SpecificDates []int `json:"specific_dates,omitempty"`
|
||||
CarryOver bool `json:"carry_over"`
|
||||
IsNegative bool `json:"is_negative"`
|
||||
TargetValue *float64 `json:"target_value,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type value_objects.HabitType `json:"type"`
|
||||
Frequency value_objects.Frequency `json:"frequency"`
|
||||
SpecificDays []int `json:"specific_days,omitempty"`
|
||||
SpecificDates []int `json:"specific_dates,omitempty"`
|
||||
CarryOver bool `json:"carry_over"`
|
||||
IsNegative bool `json:"is_negative"`
|
||||
TargetValue *float64 `json:"target_value,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateHabitRequest struct {
|
||||
@@ -28,19 +28,19 @@ type UpdateHabitRequest struct {
|
||||
}
|
||||
|
||||
type HabitResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type value_objects.HabitType `json:"type"`
|
||||
Frequency value_objects.Frequency `json:"frequency"`
|
||||
SpecificDays []int `json:"specific_days,omitempty"`
|
||||
SpecificDates []int `json:"specific_dates,omitempty"`
|
||||
CarryOver bool `json:"carry_over"`
|
||||
IsNegative bool `json:"is_negative"`
|
||||
TargetValue *float64 `json:"target_value,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type value_objects.HabitType `json:"type"`
|
||||
Frequency value_objects.Frequency `json:"frequency"`
|
||||
SpecificDays []int `json:"specific_days,omitempty"`
|
||||
SpecificDates []int `json:"specific_dates,omitempty"`
|
||||
CarryOver bool `json:"carry_over"`
|
||||
IsNegative bool `json:"is_negative"`
|
||||
TargetValue *float64 `json:"target_value,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
||||
}
|
||||
|
||||
type MarkHabitRequest struct {
|
||||
@@ -59,14 +59,14 @@ type TodaysHabitResponse struct {
|
||||
}
|
||||
|
||||
type UserHabitResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type value_objects.HabitType `json:"type"`
|
||||
Frequency value_objects.Frequency `json:"frequency"`
|
||||
SpecificDays []int `json:"specific_days,omitempty"`
|
||||
TargetValue *float64 `json:"target_value,omitempty"`
|
||||
CarryOver bool `json:"carry_over"`
|
||||
IsNegative bool `json:"is_negative"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type value_objects.HabitType `json:"type"`
|
||||
Frequency value_objects.Frequency `json:"frequency"`
|
||||
SpecificDays []int `json:"specific_days,omitempty"`
|
||||
TargetValue *float64 `json:"target_value,omitempty"`
|
||||
CarryOver bool `json:"carry_over"`
|
||||
IsNegative bool `json:"is_negative"`
|
||||
}
|
||||
|
||||
type HabitEntryResponse struct {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"apocapoc-api/internal/infrastructure/auth"
|
||||
|
||||
"github.com/go-chi/httprate"
|
||||
)
|
||||
|
||||
func RateLimitByUser(jwtService *auth.JWTService, requestsPerMinute int, duration time.Duration) func(http.Handler) http.Handler {
|
||||
limiter := httprate.NewRateLimiter(
|
||||
requestsPerMinute,
|
||||
duration,
|
||||
httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
return r.RemoteAddr, nil
|
||||
}
|
||||
return "user:" + userID, nil
|
||||
}),
|
||||
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
w.Write([]byte(`{"error":"Rate limit exceeded. Please try again later."}`))
|
||||
}),
|
||||
)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-RateLimit-Limit", strconv.Itoa(requestsPerMinute))
|
||||
|
||||
limiter.Handler(next).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@ func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *A
|
||||
|
||||
r.Route("/api/v1/habits", func(r chi.Router) {
|
||||
r.Use(AuthMiddleware(jwtService))
|
||||
r.Use(RateLimitByUser(jwtService, 100, 1*time.Minute))
|
||||
|
||||
r.Post("/", habitHandlers.CreateHabit)
|
||||
r.Get("/", habitHandlers.GetUserHabits)
|
||||
r.Get("/today", habitHandlers.GetTodaysHabits)
|
||||
@@ -59,6 +61,7 @@ func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *A
|
||||
|
||||
r.Route("/api/v1/stats", func(r chi.Router) {
|
||||
r.Use(AuthMiddleware(jwtService))
|
||||
r.Use(RateLimitByUser(jwtService, 100, 1*time.Minute))
|
||||
r.Get("/habits/{id}", statsHandlers.GetHabitStats)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user