Files
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

179 lines
3.9 KiB
Go

package auth
import (
"crypto/rand"
"encoding/hex"
"errors"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"github.com/llamalink/llamalink/internal/db"
)
const (
TokenPrefix = "llmk_"
TokenLen = 32 // 64 hex chars
)
var (
ErrInvalidKey = errors.New("invalid API key")
ErrKeyNotFound = errors.New("API key not found")
ErrKeyRevoked = errors.New("API key has been revoked")
ErrKeyExpired = errors.New("API key has expired")
ErrInsufficientScope = errors.New("insufficient scope")
)
type CreateKeyRequest struct {
Name string
Scopes []string
TokensLimit *int
WebhookURL *string
OwnerLabel *string
}
type Service struct {
db *gorm.DB
}
func NewService(db *gorm.DB) *Service {
return &Service{db: db}
}
func (s *Service) GenerateToken() (string, error) {
bytes := make([]byte, TokenLen)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return TokenPrefix + hex.EncodeToString(bytes), nil
}
func (s *Service) HashToken(token string) string {
hash, _ := bcrypt.GenerateFromPassword([]byte(token), bcrypt.DefaultCost)
return string(hash)
}
func (s *Service) Create(req CreateKeyRequest) (*db.ApiKey, string, error) {
token, err := s.GenerateToken()
if err != nil {
return nil, "", err
}
isAdmin := s.isFirstKey()
apiKey := &db.ApiKey{
ID: uuid.New(),
Name: req.Name,
KeyHash: s.HashToken(token),
KeyPrefix: token[:len(TokenPrefix)+8],
Scopes: req.Scopes,
IsActive: true,
IsAdmin: isAdmin,
OwnerLabel: req.OwnerLabel,
}
if err := s.db.Create(apiKey).Error; err != nil {
return nil, "", err
}
// Create quota if specified
if req.TokensLimit != nil && *req.TokensLimit > 0 {
now := time.Now().UTC()
periodStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
periodEnd := periodStart.AddDate(0, 1, 0)
quota := &db.Quota{
ID: uuid.New(),
ApiKeyID: apiKey.ID,
PeriodStart: periodStart,
PeriodEnd: periodEnd,
TokensLimit: *req.TokensLimit,
TokensUsed: 0,
}
s.db.Create(quota)
}
return apiKey, token, nil
}
func (s *Service) Revoke(id uuid.UUID) error {
result := s.db.Model(&db.ApiKey{}).Where("id = ?", id).Update("is_active", false)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrKeyNotFound
}
return nil
}
func (s *Service) Validate(token string) (*db.ApiKey, error) {
if len(token) < len(TokenPrefix)+8 {
return nil, ErrInvalidKey
}
prefix := token[:len(TokenPrefix)+8]
var apiKey db.ApiKey
if err := s.db.Where("key_prefix = ? AND is_active = ?", prefix, true).First(&apiKey).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrInvalidKey
}
return nil, err
}
if err := bcrypt.CompareHashAndPassword([]byte(apiKey.KeyHash), []byte(token)); err != nil {
return nil, ErrInvalidKey
}
if apiKey.ExpiresAt != nil && time.Now().After(*apiKey.ExpiresAt) {
return nil, ErrKeyExpired
}
// Update last used
now := time.Now()
s.db.Model(&apiKey).Update("last_used_at", now)
return &apiKey, nil
}
func (s *Service) List(includeInactive bool) ([]db.ApiKey, error) {
var keys []db.ApiKey
query := s.db.Order("created_at DESC")
if !includeInactive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&keys).Error; err != nil {
return nil, err
}
return keys, nil
}
func (s *Service) GetByID(id uuid.UUID) (*db.ApiKey, error) {
var key db.ApiKey
if err := s.db.Where("id = ?", id).First(&key).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrKeyNotFound
}
return nil, err
}
return &key, nil
}
func (s *Service) HasScope(key *db.ApiKey, scope string) bool {
for _, kScope := range key.Scopes {
if kScope == scope {
return true
}
}
return false
}
func (s *Service) isFirstKey() bool {
var count int64
s.db.Model(&db.ApiKey{}).Count(&count)
return count == 0
}