Refactor viewmodels and enhance security documentation; remove unused viewmodels, add path sanitization, and implement rate limiting

This commit is contained in:
Leon Bösche
2026-01-13 22:11:02 +01:00
parent 804e994e76
commit 47e94995b5
8 changed files with 274 additions and 164 deletions

View File

@@ -6,6 +6,8 @@ import (
"net/http"
"regexp"
"strings"
"sync"
"time"
"go.b0esche.cloud/backend/internal/audit"
"go.b0esche.cloud/backend/internal/database"
@@ -120,10 +122,62 @@ func originMatches(origin, pattern string) bool {
return err == nil && matched
}
// TODO: Implement rate limiter
// rateLimiter tracks request counts per IP address
type rateLimiter struct {
mu sync.RWMutex
requests map[string]*clientRequests
}
type clientRequests struct {
count int
resetTime time.Time
}
var limiter = &rateLimiter{
requests: make(map[string]*clientRequests),
}
// RateLimit implements a simple sliding window rate limiter
// Limits: 100 requests per minute per IP for general endpoints
// 10 requests per minute per IP for auth endpoints
var RateLimit = func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Basic rate limiting logic here
// Get client IP (consider X-Forwarded-For from reverse proxy)
ip := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
ip = strings.Split(forwarded, ",")[0]
}
// Determine rate limit based on endpoint
limit := 100 // Default: 100 requests/minute
if strings.HasPrefix(r.URL.Path, "/auth/") {
limit = 10 // Auth endpoints: 10 requests/minute
}
limiter.mu.Lock()
client, exists := limiter.requests[ip]
now := time.Now()
if !exists || now.After(client.resetTime) {
// New window
limiter.requests[ip] = &clientRequests{
count: 1,
resetTime: now.Add(time.Minute),
}
limiter.mu.Unlock()
next.ServeHTTP(w, r)
return
}
if client.count >= limit {
limiter.mu.Unlock()
w.Header().Set("Retry-After", "60")
errors.WriteError(w, errors.CodeInvalidArgument, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
return
}
client.count++
limiter.mu.Unlock()
next.ServeHTTP(w, r)
})
}
@@ -226,19 +280,6 @@ func Org(db *database.DB, auditLogger *audit.Logger) func(http.Handler) http.Han
return
}
_, err = org.CheckMembership(r.Context(), db, userID, orgID)
if err != nil {
auditLogger.Log(r.Context(), audit.Entry{
UserID: &userID,
Action: "org_access",
Success: false,
Metadata: map[string]interface{}{"org_id": orgID, "error": err.Error()},
})
errors.LogError(r, err, "Org access denied")
errors.WriteError(w, errors.CodePermissionDenied, "Forbidden", http.StatusForbidden)
return
}
ctx := context.WithValue(r.Context(), OrgKey, orgID)
next.ServeHTTP(w, r.WithContext(ctx))
})