l_ai_knowledge/internal/application/service/tenant.go

298 lines
8.1 KiB
Go

package service
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"errors"
"knowlege-lsxd/internal/logger"
"knowlege-lsxd/internal/types"
"knowlege-lsxd/internal/types/interfaces"
"io"
"os"
"strings"
"time"
)
var apiKeySecret = getApiKeySecret()
// ListTenantsParams defines parameters for listing tenants with filtering and pagination
type ListTenantsParams struct {
Page int // Page number for pagination
PageSize int // Number of items per page
Status string // Filter by tenant status
Name string // Filter by tenant name
}
// tenantService implements the TenantService interface
type tenantService struct {
repo interfaces.TenantRepository // Repository for tenant data operations
}
// NewTenantService creates a new tenant service instance
func NewTenantService(repo interfaces.TenantRepository) interfaces.TenantService {
return &tenantService{repo: repo}
}
// CreateTenant creates a new tenant
func (s *tenantService) CreateTenant(ctx context.Context, tenant *types.Tenant) (*types.Tenant, error) {
logger.Info(ctx, "Start creating tenant")
if tenant.Name == "" {
logger.Error(ctx, "Tenant name cannot be empty")
return nil, errors.New("tenant name cannot be empty")
}
logger.Infof(ctx, "Creating tenant, name: %s", tenant.Name)
// Create tenant with initial values
tenant.APIKey = s.generateApiKey(0)
tenant.Status = "active"
tenant.CreatedAt = time.Now()
tenant.UpdatedAt = time.Now()
logger.Info(ctx, "Saving tenant information to database")
if err := s.repo.CreateTenant(ctx, tenant); err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"tenant_name": tenant.Name,
})
return nil, err
}
logger.Infof(ctx, "Tenant created successfully, ID: %d, generating official API Key", tenant.ID)
tenant.APIKey = s.generateApiKey(tenant.ID)
if err := s.repo.UpdateTenant(ctx, tenant); err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"tenant_id": tenant.ID,
"tenant_name": tenant.Name,
})
return nil, err
}
logger.Infof(ctx, "Tenant creation and update completed, ID: %d, name: %s", tenant.ID, tenant.Name)
return tenant, nil
}
// GetTenantByID retrieves a tenant by their ID
func (s *tenantService) GetTenantByID(ctx context.Context, id uint) (*types.Tenant, error) {
logger.Info(ctx, "Start retrieving tenant")
if id == 0 {
logger.Error(ctx, "Tenant ID cannot be 0")
return nil, errors.New("tenant ID cannot be 0")
}
logger.Infof(ctx, "Retrieving tenant, ID: %d", id)
tenant, err := s.repo.GetTenantByID(ctx, id)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"tenant_id": id,
})
return nil, err
}
logger.Infof(ctx, "Tenant retrieved successfully, ID: %d, name: %s", tenant.ID, tenant.Name)
return tenant, nil
}
// ListTenants retrieves a list of all tenants
func (s *tenantService) ListTenants(ctx context.Context) ([]*types.Tenant, error) {
logger.Info(ctx, "Start retrieving tenant list")
tenants, err := s.repo.ListTenants(ctx)
if err != nil {
logger.ErrorWithFields(ctx, err, nil)
return nil, err
}
logger.Infof(ctx, "Tenant list retrieved successfully, total: %d", len(tenants))
return tenants, nil
}
// UpdateTenant updates an existing tenant's information
func (s *tenantService) UpdateTenant(ctx context.Context, tenant *types.Tenant) (*types.Tenant, error) {
if tenant.ID == 0 {
logger.Error(ctx, "Tenant ID cannot be 0")
return nil, errors.New("tenant ID cannot be 0")
}
logger.Infof(ctx, "Updating tenant, ID: %d, name: %s", tenant.ID, tenant.Name)
// Generate new API key if empty
if tenant.APIKey == "" {
logger.Info(ctx, "API Key is empty, generating new API Key")
tenant.APIKey = s.generateApiKey(tenant.ID)
}
tenant.UpdatedAt = time.Now()
logger.Info(ctx, "Saving tenant information to database")
if err := s.repo.UpdateTenant(ctx, tenant); err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"tenant_id": tenant.ID,
})
return nil, err
}
logger.Infof(ctx, "Tenant updated successfully, ID: %d", tenant.ID)
return tenant, nil
}
// DeleteTenant removes a tenant by their ID
func (s *tenantService) DeleteTenant(ctx context.Context, id uint) error {
logger.Info(ctx, "Start deleting tenant")
if id == 0 {
logger.Error(ctx, "Tenant ID cannot be 0")
return errors.New("tenant ID cannot be 0")
}
logger.Infof(ctx, "Deleting tenant, ID: %d", id)
// Get tenant information for logging
tenant, err := s.repo.GetTenantByID(ctx, id)
if err != nil {
if err.Error() == "record not found" {
logger.Warnf(ctx, "Tenant to be deleted does not exist, ID: %d", id)
} else {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"tenant_id": id,
})
return err
}
} else {
logger.Infof(ctx, "Deleting tenant, ID: %d, name: %s", id, tenant.Name)
}
err = s.repo.DeleteTenant(ctx, id)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"tenant_id": id,
})
return err
}
logger.Infof(ctx, "Tenant deleted successfully, ID: %d", id)
return nil
}
// UpdateAPIKey updates the API key for a specific tenant
func (s *tenantService) UpdateAPIKey(ctx context.Context, id uint) (string, error) {
logger.Info(ctx, "Start updating tenant API Key")
if id == 0 {
logger.Error(ctx, "Tenant ID cannot be 0")
return "", errors.New("tenant ID cannot be 0")
}
logger.Infof(ctx, "Retrieving tenant information, ID: %d", id)
tenant, err := s.repo.GetTenantByID(ctx, id)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"tenant_id": id,
})
return "", err
}
logger.Infof(ctx, "Generating new API Key for tenant, ID: %d", id)
tenant.APIKey = s.generateApiKey(tenant.ID)
if err := s.repo.UpdateTenant(ctx, tenant); err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"tenant_id": id,
})
return "", err
}
logger.Infof(ctx, "Tenant API Key updated successfully, ID: %d", id)
return tenant.APIKey, nil
}
// generateApiKey generates a secure API key for tenant authentication
func (r *tenantService) generateApiKey(tenantID uint) string {
// 1. Convert tenant_id to bytes
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(tenantID))
// 2. Encrypt tenant_id using AES-GCM
block, err := aes.NewCipher(apiKeySecret)
if err != nil {
panic("Failed to create AES cipher: " + err.Error())
}
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
panic(err.Error())
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
panic("Failed to create GCM cipher: " + err.Error())
}
ciphertext := aesgcm.Seal(nil, nonce, idBytes, nil)
// 3. Combine nonce and ciphertext, then encode with base64
combined := append(nonce, ciphertext...)
encoded := base64.RawURLEncoding.EncodeToString(combined)
// Create final API Key in format: sk-{encrypted_part}
return "sk-" + encoded
}
// ExtractTenantIDFromAPIKey extracts the tenant ID from an API key
func (r *tenantService) ExtractTenantIDFromAPIKey(apiKey string) (uint, error) {
// 1. Validate format and extract encrypted part
parts := strings.SplitN(apiKey, "-", 2)
if len(parts) != 2 || parts[0] != "sk" {
return 0, errors.New("invalid API key format")
}
// 2. Decode the base64 part
encryptedData, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return 0, errors.New("invalid API key encoding")
}
// 3. Separate nonce and ciphertext
if len(encryptedData) < 12 {
return 0, errors.New("invalid API key length")
}
nonce, ciphertext := encryptedData[:12], encryptedData[12:]
// 4. Decrypt
block, err := aes.NewCipher(apiKeySecret)
if err != nil {
return 0, errors.New("decryption error")
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return 0, errors.New("decryption error")
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return 0, errors.New("API key is invalid or has been tampered with")
}
// 5. Convert back to tenant_id
tenantID := binary.LittleEndian.Uint64(plaintext)
return uint(tenantID), nil
}
func getApiKeySecret() []byte {
secret := os.Getenv("TENANT_AES_KEY")
hashArray := sha256.Sum256([]byte(secret))
return hashArray[:]
}