Files
llama-link/internal/api/middleware/auth.go
T
darroyo 4c9ed3c24b 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.)
2026-07-30 10:58:55 -04:00

157 lines
3.2 KiB
Go

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)
}