4c9ed3c24b
Complete rewrite from Python/FastAPI to Go/Gin: - Go backend: auth (API keys + bcrypt), llama.cpp subprocess manager, hot-swap multi-model, rate limiting, quota system, webhooks - Vue 3 SPA admin panel (src/) with Tailwind CSS - Deployment: Docker multi-stage, docker-compose, nginx, systemd - GORM/SQLite models: ApiKey, Model, UsageLog, Quota, Webhook - REST API: /api/v1/admin/* (keys, models, chat, usage, health) - Embedded frontend via go:embed (build output at web/dist/) Removed legacy Python artifacts (app/, tests/, pyproject.toml, etc.)
86 lines
1.5 KiB
Go
86 lines
1.5 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
type RateLimiter struct {
|
|
visitors map[string]*visitor
|
|
mu sync.RWMutex
|
|
rate rate.Limit
|
|
burst int
|
|
}
|
|
|
|
type visitor struct {
|
|
limiter *rate.Limiter
|
|
lastSeen time.Time
|
|
}
|
|
|
|
func NewRateLimiter(requestsPerMinute int) *RateLimiter {
|
|
rl := &RateLimiter{
|
|
visitors: make(map[string]*visitor),
|
|
rate: rate.Limit(float64(requestsPerMinute) / 60.0),
|
|
burst: requestsPerMinute / 10,
|
|
}
|
|
if requestsPerMinute < 10 {
|
|
rl.burst = 1
|
|
}
|
|
|
|
// Cleanup old visitors
|
|
go rl.cleanup()
|
|
|
|
return rl
|
|
}
|
|
|
|
func (rl *RateLimiter) cleanup() {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
for range ticker.C {
|
|
rl.mu.Lock()
|
|
for ip, v := range rl.visitors {
|
|
if time.Since(v.lastSeen) > 10*time.Minute {
|
|
delete(rl.visitors, ip)
|
|
}
|
|
}
|
|
rl.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func (rl *RateLimiter) getVisitor(ip string) *rate.Limiter {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
v, exists := rl.visitors[ip]
|
|
if !exists {
|
|
v = &visitor{
|
|
limiter: rate.NewLimiter(rl.rate, rl.burst),
|
|
lastSeen: time.Now(),
|
|
}
|
|
rl.visitors[ip] = v
|
|
}
|
|
|
|
v.lastSeen = time.Now()
|
|
return v.limiter
|
|
}
|
|
|
|
func RateLimitMiddleware(rl *RateLimiter) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
ip := c.ClientIP()
|
|
if !rl.getVisitor(ip).Allow() {
|
|
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
|
|
"error": gin.H{
|
|
"code": "rate_limit_exceeded",
|
|
"message": "Too many requests",
|
|
"retry_after_seconds": 60,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|