package apikey import ( "encoding/json" "errors" "time" "go.uber.org/zap" "gorm.io/gorm" "openteam/server/internal/pkg/crypto" "openteam/server/internal/pkg/rand" "openteam/server/internal/store" ) var ErrKeyNotFound = errors.New("api key not found") type Service struct { db *gorm.DB log *zap.Logger } func NewService(db *gorm.DB, log *zap.Logger) *Service { return &Service{db: db, log: log} } type CreateInput struct { Name string `json:"name" binding:"required,max=128"` QuotaTokensPerDay *int64 `json:"quotaTokensPerDay"` QuotaRequestsPerDay *int `json:"quotaRequestsPerDay"` AllowedModels []string `json:"allowedModels"` ExpiresInDays *int `json:"expiresInDays"` } type UpdateInput struct { Name *string `json:"name"` QuotaTokensPerDay *int64 `json:"quotaTokensPerDay"` QuotaRequestsPerDay *int `json:"quotaRequestsPerDay"` AllowedModels []string `json:"allowedModels"` ExpiresAt *time.Time `json:"expiresAt"` Status *string `json:"status"` } // Create generates a key and returns it once along with the stored record. func (s *Service) Create(userID int64, in CreateInput) (*store.ApiKey, string, error) { plain, err := rand.Base62(48) if err != nil { return nil, "", err } full := "sk-" + plain rec := &store.ApiKey{ UserID: userID, Name: in.Name, KeyHash: crypto.HashSHA256(full), KeyPrefix: "sk-" + plain[:8], QuotaTokensPerDay: in.QuotaTokensPerDay, QuotaRequestsPerDay: in.QuotaRequestsPerDay, AllowedModels: in.AllowedModels, Status: "active", } if in.ExpiresInDays != nil && *in.ExpiresInDays > 0 { t := time.Now().AddDate(0, 0, *in.ExpiresInDays) rec.ExpiresAt = &t } if err := s.db.Create(rec).Error; err != nil { return nil, "", err } return rec, full, nil } // List returns the user's keys (never the hash). func (s *Service) List(userID int64) ([]store.ApiKey, error) { var keys []store.ApiKey err := s.db.Where("user_id = ?", userID).Order("id DESC").Find(&keys).Error return keys, err } // Update patches a key owned by the user. func (s *Service) Update(userID, keyID int64, in UpdateInput) (*store.ApiKey, error) { var k store.ApiKey if err := s.db.First(&k, "id = ? AND user_id = ?", keyID, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrKeyNotFound } return nil, err } updates := map[string]any{} if in.Name != nil { updates["name"] = *in.Name } if in.QuotaTokensPerDay != nil { updates["quota_tokens_per_day"] = *in.QuotaTokensPerDay } if in.QuotaRequestsPerDay != nil { updates["quota_requests_per_day"] = *in.QuotaRequestsPerDay } if in.AllowedModels != nil { // GORM map updates skip the json serializer, so encode explicitly. modelsJSON, err := json.Marshal(in.AllowedModels) if err != nil { return nil, err } updates["allowed_models"] = string(modelsJSON) } if in.ExpiresAt != nil { updates["expires_at"] = in.ExpiresAt } if in.Status != nil { updates["status"] = *in.Status } if len(updates) > 0 { if err := s.db.Model(&k).Updates(updates).Error; err != nil { return nil, err } } return s.Get(userID, keyID) } // Delete revokes a key (soft revoke by status). func (s *Service) Delete(userID, keyID int64) error { res := s.db.Model(&store.ApiKey{}). Where("id = ? AND user_id = ?", keyID, userID). Update("status", "revoked") if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return ErrKeyNotFound } return nil } // Get loads one key owned by the user. func (s *Service) Get(userID, keyID int64) (*store.ApiKey, error) { var k store.ApiKey if err := s.db.First(&k, "id = ? AND user_id = ?", keyID, userID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrKeyNotFound } return nil, err } return &k, nil } // Public returns a DTO without the hash. func Public(k *store.ApiKey) map[string]any { return map[string]any{ "id": k.ID, "userId": k.UserID, "name": k.Name, "keyPrefix": k.KeyPrefix, "quotaTokensPerDay": k.QuotaTokensPerDay, "quotaRequestsPerDay": k.QuotaRequestsPerDay, "allowedModels": k.AllowedModels, "expiresAt": k.ExpiresAt, "status": k.Status, "lastUsedAt": k.LastUsedAt, "createdAt": k.CreatedAt, } }