feat: Sentinela v0.2.0 — Brazilian Financial Data API in Go
- 20 Go source files, single 16MB binary - SQLite + FTS5 full-text search (pure Go, no CGO) - BCB integration: Selic, CDI, IPCA, USD/BRL, EUR/BRL - CVM integration: 2,524 companies from registry - Fiber v2 REST API with 42 handlers - Auto-seeds on first run (~5s for BCB + CVM) - Token bucket rate limiter, optional API key auth - Periodic sync scheduler (configurable) - Graceful shutdown, structured logging (slog) - All endpoints tested with real data
This commit is contained in:
26
internal/api/middleware/apikey.go
Normal file
26
internal/api/middleware/apikey.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func NewAPIKeyAuth(apiKey string) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if c.Path() == "/health" {
|
||||
return c.Next()
|
||||
}
|
||||
key := c.Get("X-API-Key")
|
||||
if key == "" {
|
||||
auth := c.Get("Authorization")
|
||||
if strings.HasPrefix(auth, "Bearer ") {
|
||||
key = strings.TrimPrefix(auth, "Bearer ")
|
||||
}
|
||||
}
|
||||
if key != apiKey {
|
||||
return c.Status(401).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
55
internal/api/middleware/ratelimit.go
Normal file
55
internal/api/middleware/ratelimit.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type bucket struct {
|
||||
tokens float64
|
||||
lastCheck time.Time
|
||||
}
|
||||
|
||||
type rateLimiter struct {
|
||||
mu sync.Mutex
|
||||
buckets map[string]*bucket
|
||||
rate float64 // tokens per second
|
||||
capacity float64
|
||||
}
|
||||
|
||||
func NewRateLimiter(requestsPerMinute int) fiber.Handler {
|
||||
rl := &rateLimiter{
|
||||
buckets: make(map[string]*bucket),
|
||||
rate: float64(requestsPerMinute) / 60.0,
|
||||
capacity: float64(requestsPerMinute),
|
||||
}
|
||||
|
||||
return func(c *fiber.Ctx) error {
|
||||
ip := c.IP()
|
||||
rl.mu.Lock()
|
||||
b, ok := rl.buckets[ip]
|
||||
if !ok {
|
||||
b = &bucket{tokens: rl.capacity, lastCheck: time.Now()}
|
||||
rl.buckets[ip] = b
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(b.lastCheck).Seconds()
|
||||
b.tokens += elapsed * rl.rate
|
||||
if b.tokens > rl.capacity {
|
||||
b.tokens = rl.capacity
|
||||
}
|
||||
b.lastCheck = now
|
||||
|
||||
if b.tokens < 1 {
|
||||
rl.mu.Unlock()
|
||||
return c.Status(429).JSON(fiber.Map{"error": "rate limit exceeded"})
|
||||
}
|
||||
b.tokens--
|
||||
rl.mu.Unlock()
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user