Files
flowsight/backend/internal/api/ratelimit.go
T
asepharyana 1ec860f9be fix: audit round 3 — IDOR report-scoping, auth rate-limit, briefing chat scoping, DailyVolumes dedup, N+1 list routines, FE export/ask error handling
- IDOR: interrogate without report_id now scoped to user_key (LatestReportForUser); regression test TestInterrogateIDORScoped
- auth: signup/login per-IP rate limit 10/60s (new internal/api/ratelimit.go) + test
- chat unscoped grounding: build caller's own briefing instead of global LatestBriefing
- DailyVolumes: dedupe by bar date (snapshot rows hold 30-day windows) — fixes volume-anomaly skew
- GetDestination: direct (id,user_key) query instead of listing all
- ListRoutines: single LastRunsByRoutine query instead of N+1 RunHistory
- FE: exportMd/ask/HTML/PDF export now surface errors; alerts create clears channels
2026-09-16 14:12:32 +07:00

94 lines
2.0 KiB
Go

// Package api — rate limit helpers for auth endpoints.
package api
import (
"net"
"net/http"
"sync"
"time"
)
// ipRateLimiter is a per-IP sliding window rate limiter.
type ipRateLimiter struct {
mu sync.Mutex
windows map[string]*window
limit int
windowSz time.Duration
}
type window struct {
hits []time.Time
}
func newIPRateLimiter(limit int, windowSz time.Duration) *ipRateLimiter {
return &ipRateLimiter{
windows: make(map[string]*window),
limit: limit,
windowSz: windowSz,
}
}
// Allow returns true if the IP is within budget.
func (rl *ipRateLimiter) Allow(ip string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
w, ok := rl.windows[ip]
if !ok {
w = &window{}
rl.windows[ip] = w
}
// Trim entries outside the window.
cutoff := now.Add(-rl.windowSz)
n := 0
for _, t := range w.hits {
if t.After(cutoff) {
w.hits[n] = t
n++
}
}
w.hits = w.hits[:n]
if len(w.hits) >= rl.limit {
return false
}
w.hits = append(w.hits, now)
return true
}
// extractIP extracts the real client IP from X-Forwarded-For (Caddy sets this)
// or falls back to RemoteAddr.
func extractIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
// First IP in the chain is the original client.
if ip := net.ParseIP(xff[:len(xff)]); ip != nil {
return xff
}
// Handle comma-separated: take first.
for i := 0; i < len(xff); i++ {
if xff[i] == ',' {
return xff[:i]
}
}
return xff
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}
// rateLimitAuth is middleware that limits auth-related endpoints per IP:
// max requests per sliding window.
func (s *Server) rateLimitAuth(rl *ipRateLimiter, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := extractIP(r)
if !rl.Allow(ip) {
writeErr(w, http.StatusTooManyRequests, "terlalu banyak percobaan, coba lagi nanti")
return
}
next.ServeHTTP(w, r)
})
}