feat: tiered API plans (Free/Bronze/Gold/Platinum)
- Free: $0, 30 req/min, market data only - Bronze: $29/mo, 100 req/min, + companies & search - Gold: $99/mo, 500 req/min, + filings & historical - Platinum: $299/mo, 2000 req/min, all features + priority - Plan-aware rate limiting per API key (or per IP for free) - Usage tracking with daily aggregation - GET /api/v1/plans — plan listing - POST /api/v1/plans/register — instant free API key - GET /api/v1/plans/usage — usage stats - /pricing — dark-themed HTML pricing page - X-RateLimit-* and X-Plan headers on every response - Restricted endpoints return upgrade prompt - Updated OpenAPI spec with security scheme - 53 handlers, compiles clean
This commit is contained in:
100
internal/api/handlers/plans.go
Normal file
100
internal/api/handlers/plans.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *Handler) ListPlans(c *fiber.Ctx) error {
|
||||
plans := []fiber.Map{
|
||||
{
|
||||
"id": "free", "name": "Free", "price": 0, "currency": "USD",
|
||||
"rate_limit": 30, "rate_limit_unit": "requests/minute",
|
||||
"features": []string{"Market data (Selic, CDI, IPCA, FX)", "Market overview endpoint", "No API key needed"},
|
||||
"restrictions": []string{"No company data", "No filing data", "No search"},
|
||||
},
|
||||
{
|
||||
"id": "bronze", "name": "Bronze", "price": 29, "currency": "USD", "billing": "monthly",
|
||||
"rate_limit": 100, "rate_limit_unit": "requests/minute",
|
||||
"features": []string{"All market data", "Company data & search", "Full-text search", "API key authentication"},
|
||||
"restrictions": []string{"No filing data", "No bulk export"},
|
||||
},
|
||||
{
|
||||
"id": "gold", "name": "Gold", "price": 99, "currency": "USD", "billing": "monthly",
|
||||
"rate_limit": 500, "rate_limit_unit": "requests/minute",
|
||||
"features": []string{"All market data", "Company data & search", "Filing data", "Full search", "Historical data", "Bulk export"},
|
||||
"restrictions": []string{"No webhooks", "No priority support"},
|
||||
},
|
||||
{
|
||||
"id": "platinum", "name": "Platinum", "price": 299, "currency": "USD", "billing": "monthly",
|
||||
"rate_limit": 2000, "rate_limit_unit": "requests/minute",
|
||||
"features": []string{"All data access", "Webhooks", "Priority support", "Custom date ranges", "Raw CSV export", "2000 req/min"},
|
||||
"restrictions": []string{},
|
||||
},
|
||||
}
|
||||
return c.JSON(fiber.Map{"plans": plans})
|
||||
}
|
||||
|
||||
func (h *Handler) PlanUsage(c *fiber.Ctx) error {
|
||||
apiKey := c.Get("X-API-Key")
|
||||
if apiKey == "" {
|
||||
auth := c.Get("Authorization")
|
||||
if strings.HasPrefix(auth, "Bearer ") {
|
||||
apiKey = strings.TrimPrefix(auth, "Bearer ")
|
||||
}
|
||||
}
|
||||
if apiKey == "" {
|
||||
return c.Status(401).JSON(fiber.Map{"error": "API key required to view usage"})
|
||||
}
|
||||
|
||||
ak, err := h.db.GetAPIKey(apiKey)
|
||||
if err != nil {
|
||||
return c.Status(401).JSON(fiber.Map{"error": "invalid API key"})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
from := c.Query("from", now.AddDate(0, 0, -30).Format("2006-01-02"))
|
||||
to := c.Query("to", now.Format("2006-01-02"))
|
||||
|
||||
stats, err := h.db.GetUsageStats(ak.ID, from, to)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"plan": ak.Plan,
|
||||
"rate_limit": ak.RateLimit,
|
||||
"requests_today": ak.RequestsToday,
|
||||
"requests_month": ak.RequestsMonth,
|
||||
"usage": stats,
|
||||
"period": fiber.Map{"from": from, "to": to},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterPlan(c *fiber.Ctx) error {
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if body.Name == "" || body.Email == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "name and email are required"})
|
||||
}
|
||||
|
||||
ak, err := h.db.CreateAPIKey(body.Name, body.Email, "free")
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "failed to create API key"})
|
||||
}
|
||||
|
||||
return c.Status(201).JSON(fiber.Map{
|
||||
"message": "API key created successfully",
|
||||
"api_key": ak.Key,
|
||||
"plan": ak.Plan,
|
||||
"rate_limit": ak.RateLimit,
|
||||
"note": "Store your API key securely. Include it in requests as X-API-Key header.",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user