Initial commit: LlamaLink Go rewrite
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.)
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/llamalink/llamalink/internal/auth"
|
||||
"github.com/llamalink/llamalink/internal/db"
|
||||
)
|
||||
|
||||
const (
|
||||
ApiKeyCtx = "api_key"
|
||||
ApiKeyIDCtx = "api_key_id"
|
||||
)
|
||||
|
||||
func APIKeyAuth(authService *auth.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"code": "invalid_api_key",
|
||||
"message": "Authorization header required",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if token == authHeader {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"code": "invalid_api_key",
|
||||
"message": "Bearer token required",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
apiKey, err := authService.Validate(token)
|
||||
if err != nil {
|
||||
code := "invalid_api_key"
|
||||
status := http.StatusUnauthorized
|
||||
if err == auth.ErrKeyRevoked || err == auth.ErrKeyExpired {
|
||||
code = "api_key_revoked"
|
||||
status = http.StatusUnauthorized
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(status, gin.H{
|
||||
"error": gin.H{
|
||||
"code": code,
|
||||
"message": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(ApiKeyCtx, apiKey)
|
||||
c.Set(ApiKeyIDCtx, apiKey.ID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RequireScope(authService *auth.Service, scope string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
apiKey, exists := c.Get(ApiKeyCtx)
|
||||
if !exists {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"code": "invalid_api_key",
|
||||
"message": "Authentication required",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
key := apiKey.(*db.ApiKey)
|
||||
if !authService.HasScope(key, scope) {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": gin.H{
|
||||
"code": "insufficient_scope",
|
||||
"message": "API key lacks required scope: " + scope,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AdminOnly() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
apiKey, exists := c.Get(ApiKeyCtx)
|
||||
if !exists {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"code": "unauthorized",
|
||||
"message": "Admin access required",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
key := apiKey.(*db.ApiKey)
|
||||
if !key.IsAdmin {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": gin.H{
|
||||
"code": "unauthorized",
|
||||
"message": "Admin access required",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AdminTokenAuth(token string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
adminToken := c.GetHeader("X-Admin-Token")
|
||||
if adminToken == "" {
|
||||
adminToken = c.GetHeader("Authorization")
|
||||
adminToken = strings.TrimPrefix(adminToken, "Bearer ")
|
||||
}
|
||||
|
||||
if adminToken != token {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"code": "unauthorized",
|
||||
"message": "Invalid admin token",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func GetAPIKeyID(c *gin.Context) uuid.UUID {
|
||||
id, _ := c.Get(ApiKeyIDCtx)
|
||||
return id.(uuid.UUID)
|
||||
}
|
||||
|
||||
func GetAPIKey(c *gin.Context) *db.ApiKey {
|
||||
key, _ := c.Get(ApiKeyCtx)
|
||||
if key == nil {
|
||||
return nil
|
||||
}
|
||||
return key.(*db.ApiKey)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user