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.)
123 lines
2.8 KiB
Go
123 lines
2.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/llamalink/llamalink/internal/api/middleware"
|
|
"github.com/llamalink/llamalink/internal/auth"
|
|
)
|
|
|
|
type KeysHandler struct {
|
|
authService *auth.Service
|
|
}
|
|
|
|
func NewKeysHandler(authService *auth.Service) *KeysHandler {
|
|
return &KeysHandler{authService: authService}
|
|
}
|
|
|
|
type CreateKeyRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Scopes []string `json:"scopes"`
|
|
TokensLimit *int `json:"tokens_limit"`
|
|
WebhookURL *string `json:"webhook_url"`
|
|
OwnerLabel *string `json:"owner_label"`
|
|
}
|
|
|
|
func (h *KeysHandler) ListKeys(c *gin.Context) {
|
|
includeInactive := c.Query("include_inactive") == "true"
|
|
|
|
keys, err := h.authService.List(includeInactive)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Don't expose key hash
|
|
safeKeys := make([]gin.H, len(keys))
|
|
for i, k := range keys {
|
|
safeKeys[i] = gin.H{
|
|
"id": k.ID,
|
|
"name": k.Name,
|
|
"key_prefix": k.KeyPrefix,
|
|
"scopes": k.Scopes,
|
|
"is_active": k.IsActive,
|
|
"is_admin": k.IsAdmin,
|
|
"owner_label": k.OwnerLabel,
|
|
"created_at": k.CreatedAt,
|
|
"last_used_at": k.LastUsedAt,
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, safeKeys)
|
|
}
|
|
|
|
func (h *KeysHandler) CreateKey(c *gin.Context) {
|
|
var req CreateKeyRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if req.Scopes == nil {
|
|
req.Scopes = []string{"chat", "models", "usage"}
|
|
}
|
|
|
|
apiKey, rawKey, err := h.authService.Create(auth.CreateKeyRequest{
|
|
Name: req.Name,
|
|
Scopes: req.Scopes,
|
|
TokensLimit: req.TokensLimit,
|
|
WebhookURL: req.WebhookURL,
|
|
OwnerLabel: req.OwnerLabel,
|
|
})
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"id": apiKey.ID,
|
|
"name": apiKey.Name,
|
|
"key": rawKey,
|
|
"key_prefix": apiKey.KeyPrefix,
|
|
"scopes": apiKey.Scopes,
|
|
"is_admin": apiKey.IsAdmin,
|
|
"created_at": apiKey.CreatedAt,
|
|
})
|
|
}
|
|
|
|
func (h *KeysHandler) RevokeKey(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid key id"})
|
|
return
|
|
}
|
|
|
|
// Can't revoke own key
|
|
currentKey := middleware.GetAPIKey(c)
|
|
if currentKey.ID == id {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": gin.H{
|
|
"code": "validation_error",
|
|
"message": "Cannot revoke your own admin key",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := h.authService.Revoke(id); err != nil {
|
|
if err == auth.ErrKeyNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "key not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusNoContent)
|
|
}
|